Skip to main content

linera_client/
chain_listener.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{btree_map::Entry, BTreeMap, BTreeSet},
6    sync::Arc,
7    time::Duration,
8};
9
10use futures::{future, lock::Mutex, Future, FutureExt as _, StreamExt};
11use linera_base::{
12    crypto::{CryptoHash, Signer},
13    data_types::{ChainDescription, Epoch, MessagePolicy, TimeDelta, Timestamp},
14    identifiers::{AccountOwner, BlobType, ChainId},
15    ownership::ChainOwnership,
16    util::future::FutureSyncExt as _,
17    Task,
18};
19use linera_core::{
20    client::{
21        chain_client::{self, ChainClient},
22        AbortOnDrop, ListeningMode,
23    },
24    node::NotificationStream,
25    worker::{Notification, Reason},
26    Environment, Wallet,
27};
28use linera_storage::{Arc as CacheArc, Storage as _};
29use tokio::sync::{mpsc::UnboundedReceiver, Notify};
30use tokio_util::sync::CancellationToken;
31use tracing::{debug, error, info, instrument, warn, Instrument as _};
32
33use crate::error::{self, Error};
34
35/// The configuration for the chain listener.
36#[derive(Debug, Clone, clap::Args, serde::Serialize, serde::Deserialize, tsify::Tsify)]
37#[serde(rename_all = "camelCase")]
38pub struct ChainListenerConfig {
39    /// Do not create blocks automatically to receive incoming messages. Instead, wait for
40    /// an explicit mutation `processInbox`.
41    #[serde(default)]
42    #[arg(
43        long = "listener-skip-process-inbox",
44        env = "LINERA_LISTENER_SKIP_PROCESS_INBOX"
45    )]
46    pub skip_process_inbox: bool,
47
48    /// Wait before processing any notification (useful for testing).
49    #[serde(default)]
50    #[arg(
51        long = "listener-delay-before-ms",
52        default_value = "0",
53        env = "LINERA_LISTENER_DELAY_BEFORE"
54    )]
55    pub delay_before_ms: u64,
56
57    /// Wait after processing any notification (useful for rate limiting).
58    #[serde(default)]
59    #[arg(
60        long = "listener-delay-after-ms",
61        default_value = "0",
62        env = "LINERA_LISTENER_DELAY_AFTER"
63    )]
64    pub delay_after_ms: u64,
65
66    /// The time between two background received-certificate syncs of the same chain, in
67    /// milliseconds. Repeating the sync keeps the received-certificate trackers fresh,
68    /// so a process restart only has to walk the short backlog accumulated since the
69    /// last refresh instead of everything since the previous restart. Set to 0 to sync
70    /// only once, when the chain listener starts.
71    #[serde(default = "default_background_sync_interval_ms")]
72    #[arg(
73        long = "listener-background-sync-interval-ms",
74        default_value = "900000",
75        env = "LINERA_LISTENER_BACKGROUND_SYNC_INTERVAL_MS"
76    )]
77    pub background_sync_interval_ms: u64,
78}
79
80/// The default value of [`ChainListenerConfig::background_sync_interval_ms`]: 15 minutes.
81fn default_background_sync_interval_ms() -> u64 {
82    900_000
83}
84
85// Written out rather than derived: `#[derive(Default)]` would give
86// `background_sync_interval_ms = 0`, which means "never repeat the sync" and so disagrees
87// with the clap and serde defaults. Callers that build a config with `..Default::default()`
88// would silently opt out of the periodic sync.
89impl Default for ChainListenerConfig {
90    fn default() -> Self {
91        Self {
92            skip_process_inbox: false,
93            delay_before_ms: 0,
94            delay_after_ms: 0,
95            background_sync_interval_ms: default_background_sync_interval_ms(),
96        }
97    }
98}
99
100type ContextChainClient<C> = ChainClient<<C as ClientContext>::Environment>;
101
102/// The context in which a chain listener operates, providing access to the wallet, storage and client.
103#[cfg_attr(not(web), trait_variant::make(Send + Sync))]
104#[allow(async_fn_in_trait)]
105pub trait ClientContext {
106    /// The execution environment used by the client.
107    type Environment: linera_core::Environment;
108
109    /// Returns a reference to the wallet.
110    fn wallet(&self) -> &<Self::Environment as linera_core::Environment>::Wallet;
111
112    /// Returns a reference to the storage.
113    fn storage(&self) -> &<Self::Environment as linera_core::Environment>::Storage;
114
115    /// Returns a reference to the client.
116    fn client(&self) -> &Arc<linera_core::client::Client<Self::Environment>>;
117
118    /// Returns the ID of the admin chain.
119    fn admin_chain_id(&self) -> ChainId {
120        self.client().admin_chain_id()
121    }
122
123    /// Gets the timing sender for benchmarking, if available.
124    #[cfg(not(web))]
125    fn timing_sender(
126        &self,
127    ) -> Option<tokio::sync::mpsc::UnboundedSender<(u64, linera_core::client::TimingType)>>;
128
129    /// Gets the timing sender for benchmarking, if available.
130    #[cfg(web)]
131    fn timing_sender(
132        &self,
133    ) -> Option<tokio::sync::mpsc::UnboundedSender<(u64, linera_core::client::TimingType)>> {
134        None
135    }
136
137    /// Creates a chain client for the chain with the given ID, using the wallet's state.
138    fn make_chain_client(
139        &self,
140        chain_id: ChainId,
141    ) -> impl Future<Output = Result<ChainClient<Self::Environment>, Error>> {
142        async move {
143            let chain = self
144                .wallet()
145                .get(chain_id)
146                .make_sync()
147                .await
148                .map_err(error::Error::wallet)?
149                .unwrap_or_default();
150            let follow_only = chain.is_follow_only();
151            Ok(self.client().create_chain_client(
152                chain_id,
153                chain.block_hash,
154                chain.next_block_height,
155                &chain.pending_fast_proposal,
156                chain.owner,
157                self.timing_sender(),
158                follow_only,
159            ))
160        }
161    }
162
163    /// Adds a newly created chain to the wallet.
164    async fn update_wallet_for_new_chain(
165        &mut self,
166        chain_id: ChainId,
167        owner: Option<AccountOwner>,
168        timestamp: Timestamp,
169        epoch: Epoch,
170    ) -> Result<(), Error>;
171
172    /// Updates the wallet with the latest state of the given chain client.
173    async fn update_wallet(&mut self, client: &ContextChainClient<Self>) -> Result<(), Error>;
174}
175
176/// Extension methods for [`ClientContext`].
177#[allow(async_fn_in_trait)]
178pub trait ClientContextExt: ClientContext {
179    /// Returns a chain client for every chain in the wallet.
180    async fn clients(&self) -> Result<Vec<ContextChainClient<Self>>, Error> {
181        use futures::stream::TryStreamExt as _;
182        self.wallet()
183            .chain_ids()
184            .map_err(error::Error::wallet)
185            .and_then(|chain_id| self.make_chain_client(chain_id))
186            .try_collect()
187            .await
188    }
189
190    /// Returns the subset of `owners` for which we have a key pair in the wallet.
191    ///
192    /// Duplicate inputs are treated as one.
193    async fn owners_with_key(
194        &self,
195        owners: impl IntoIterator<Item = AccountOwner>,
196    ) -> Result<BTreeSet<AccountOwner>, Error> {
197        let mut result = BTreeSet::new();
198        for owner in owners.into_iter().collect::<BTreeSet<_>>() {
199            if self.client().has_key_for(&owner).await? {
200                result.insert(owner);
201            }
202        }
203        Ok(result)
204    }
205
206    /// Returns the unique owner from `owners` for which we have a key pair in the wallet.
207    ///
208    /// Returns `None` when zero or multiple distinct owners match.
209    async fn unique_owner_with_key(
210        &self,
211        owners: impl IntoIterator<Item = AccountOwner>,
212    ) -> Result<Option<AccountOwner>, Error> {
213        let with_key = self.owners_with_key(owners).await?;
214        Ok(if with_key.len() == 1 {
215            with_key.into_iter().next()
216        } else {
217            None
218        })
219    }
220
221    /// Sets the preferred owner of `chain_client`'s chain if the current one is no longer
222    /// in `ownership` and we have a key pair for exactly one of the new owners.
223    async fn maybe_auto_assign_preferred_owner(
224        &self,
225        chain_client: &mut ContextChainClient<Self>,
226        ownership: &ChainOwnership,
227    ) -> Result<(), Error> {
228        let chain_id = chain_client.chain_id();
229        let old_owner = chain_client.preferred_owner();
230        if old_owner.is_some_and(|o| ownership.all_owners().any(|n| *n == o)) {
231            return Ok(());
232        }
233        let Some(new_owner) = self
234            .unique_owner_with_key(ownership.all_owners().copied())
235            .await?
236        else {
237            return Ok(());
238        };
239        info!(
240            %chain_id, ?old_owner, %new_owner,
241            "Auto-assigning preferred owner from wallet key pair",
242        );
243        chain_client.set_preferred_owner(new_owner);
244        self.wallet()
245            .modify(chain_id, |chain| chain.owner = Some(new_owner))
246            .await
247            .map_err(error::Error::wallet)?;
248        Ok(())
249    }
250}
251
252impl<T: ClientContext> ClientContextExt for T {}
253
254/// A chain client together with the stream of notifications from the local node.
255///
256/// A background task listens to the validators and updates the local node, so any updates to
257/// this chain will trigger a notification. The background task is terminated when this gets
258/// dropped.
259struct ListeningClient<C: ClientContext> {
260    /// The chain client.
261    client: ContextChainClient<C>,
262    /// The abort handle for the task that listens to the validators.
263    abort_handle: AbortOnDrop,
264    /// The listening task.
265    listener: Task<()>,
266    /// The stream of notifications from the local node.
267    notification_stream: Arc<Mutex<NotificationStream>>,
268    /// The background sync process.
269    background_sync: Task<()>,
270    /// Signal to wake the per-chain inbox processing task.
271    inbox_notify: Arc<Notify>,
272    /// The long-lived per-chain inbox processing task.
273    inbox_task: Task<()>,
274    /// Cancellation token for the per-chain inbox task (child of the global token).
275    inbox_cancellation: CancellationToken,
276}
277
278impl<C: ClientContext + 'static> ListeningClient<C> {
279    #[expect(clippy::too_many_arguments)]
280    fn new(
281        client: ContextChainClient<C>,
282        abort_handle: AbortOnDrop,
283        listener: Task<()>,
284        notification_stream: NotificationStream,
285        background_sync: Task<()>,
286        context: &Arc<Mutex<C>>,
287        config: &Arc<ChainListenerConfig>,
288        parent_cancellation: &CancellationToken,
289    ) -> Self {
290        let inbox_notify = Arc::new(Notify::new());
291        let inbox_cancellation = parent_cancellation.child_token();
292        let inbox_task =
293            Self::spawn_inbox_task(&client, context, config, &inbox_notify, &inbox_cancellation);
294        Self {
295            client,
296            abort_handle,
297            listener,
298            #[allow(clippy::arc_with_non_send_sync)] // Only `Send` with `futures-util/alloc`.
299            notification_stream: Arc::new(Mutex::new(notification_stream)),
300            background_sync,
301            inbox_notify,
302            inbox_task,
303            inbox_cancellation,
304        }
305    }
306
307    /// Respawns the per-chain inbox task with a fresh clone of the client.
308    /// The `inbox_notify` `Arc` is reused so no pending permits are lost.
309    fn respawn_inbox_task(
310        &mut self,
311        parent_cancellation: &CancellationToken,
312        context: &Arc<Mutex<C>>,
313        config: &Arc<ChainListenerConfig>,
314    ) {
315        self.inbox_cancellation.cancel();
316        self.inbox_cancellation = parent_cancellation.child_token();
317        self.inbox_task = Self::spawn_inbox_task(
318            &self.client,
319            context,
320            config,
321            &self.inbox_notify,
322            &self.inbox_cancellation,
323        );
324    }
325
326    fn spawn_inbox_task(
327        client: &ContextChainClient<C>,
328        context: &Arc<Mutex<C>>,
329        config: &Arc<ChainListenerConfig>,
330        inbox_notify: &Arc<Notify>,
331        inbox_cancellation: &CancellationToken,
332    ) -> Task<()> {
333        Task::spawn(inbox_processing_loop(
334            client.clone(),
335            Arc::clone(context),
336            Arc::clone(config),
337            Arc::clone(inbox_notify),
338            inbox_cancellation.clone(),
339        ))
340    }
341
342    async fn stop(self) {
343        // TODO(#4965): this is unnecessary: the join handle now also acts as an abort handle
344        drop(self.abort_handle);
345        self.inbox_cancellation.cancel();
346        futures::future::join3(
347            self.listener.cancel(),
348            self.background_sync.cancel(),
349            self.inbox_task.cancel(),
350        )
351        .await;
352    }
353}
354
355/// Commands to the chain listener.
356pub enum ListenerCommand {
357    /// Command: start listening to the given chains. If the chain must produce blocks,
358    /// an owner is required.
359    Listen(BTreeMap<ChainId, Option<AccountOwner>>),
360    /// Command: stop listening to the given chains.
361    StopListening(BTreeSet<ChainId>),
362    /// Command: set the message policies of some chain clients.
363    SetMessagePolicy(BTreeMap<ChainId, MessagePolicy>),
364}
365
366/// A `ChainListener` is a process that listens to notifications from validators and reacts
367/// appropriately.
368pub struct ChainListener<C: ClientContext> {
369    context: Arc<Mutex<C>>,
370    storage: <C::Environment as Environment>::Storage,
371    config: Arc<ChainListenerConfig>,
372    listening: BTreeMap<ChainId, ListeningClient<C>>,
373    cancellation_token: CancellationToken,
374    /// Map from publishing chain to subscriber chains.
375    /// Events emitted on the _publishing chain_ are of interest to the _subscriber chains_.
376    event_subscribers: BTreeMap<ChainId, BTreeSet<ChainId>>,
377    /// The channel through which the listener can receive commands.
378    command_receiver: UnboundedReceiver<ListenerCommand>,
379    /// Whether to fully sync chains in the background.
380    enable_background_sync: bool,
381}
382
383impl<C: ClientContext + 'static> ChainListener<C> {
384    /// Creates a new chain listener given client chains.
385    pub fn new(
386        config: ChainListenerConfig,
387        context: Arc<Mutex<C>>,
388        storage: <C::Environment as Environment>::Storage,
389        cancellation_token: CancellationToken,
390        command_receiver: UnboundedReceiver<ListenerCommand>,
391        enable_background_sync: bool,
392    ) -> Self {
393        Self {
394            storage,
395            context,
396            config: Arc::new(config),
397            listening: Default::default(),
398            cancellation_token,
399            event_subscribers: Default::default(),
400            command_receiver,
401            enable_background_sync,
402        }
403    }
404
405    /// Runs the chain listener.
406    #[instrument(skip(self))]
407    pub async fn run(mut self) -> Result<impl Future<Output = Result<(), Error>>, Error> {
408        let chain_ids = {
409            let guard = self.context.lock().await;
410            let admin_chain_id = guard.admin_chain_id();
411            guard
412                .make_chain_client(admin_chain_id)
413                .await?
414                .synchronize_chain_state(admin_chain_id)
415                .await?;
416            let mut chain_ids: BTreeMap<_, _> = guard
417                .wallet()
418                .items()
419                .collect::<Vec<_>>()
420                .await
421                .into_iter()
422                .map(|result| {
423                    let (chain_id, chain) = result?;
424                    let mode = if chain.is_follow_only() {
425                        ListeningMode::FollowChain
426                    } else {
427                        ListeningMode::FullChain
428                    };
429                    Ok((chain_id, mode))
430                })
431                .collect::<Result<BTreeMap<_, _>, _>>()
432                .map_err(
433                    |e: <<C::Environment as Environment>::Wallet as Wallet>::Error| {
434                        crate::error::Error::Wallet(Box::new(e) as _)
435                    },
436                )?;
437            // If the admin chain is not in the wallet, add it as follow-only since we
438            // typically don't own it.
439            chain_ids
440                .entry(admin_chain_id)
441                .or_insert(ListeningMode::FollowChain);
442            chain_ids
443        };
444
445        Ok(async move {
446            self.listen_recursively(chain_ids).await?;
447            loop {
448                match self.next_action().await? {
449                    Action::Stop => break,
450                    Action::Notification(notification) => {
451                        self.process_notification(notification).await?
452                    }
453                }
454            }
455            future::join_all(self.listening.into_values().map(|client| client.stop())).await;
456            Ok(())
457        })
458    }
459
460    /// Processes a notification, updating local chains and validators as needed.
461    async fn process_notification(&mut self, notification: Notification) -> Result<(), Error> {
462        Self::sleep(self.config.delay_before_ms).await;
463        let Some(listening_client) = self.listening.get(&notification.chain_id) else {
464            warn!(
465                ?notification,
466                "ChainListener::process_notification: got a notification without listening to the chain"
467            );
468            return Ok(());
469        };
470        let Some(listening_mode) = listening_client.client.listening_mode() else {
471            warn!(
472                ?notification,
473                "ChainListener::process_notification: chain has no listening mode"
474            );
475            return Ok(());
476        };
477
478        if !listening_mode.is_relevant(&notification.reason) {
479            debug!(
480                reason = ?notification.reason,
481                "ChainListener: ignoring notification due to listening mode"
482            );
483            return Ok(());
484        }
485        match &notification.reason {
486            Reason::NewIncomingBundle { .. } => {
487                self.maybe_notify_inbox_processing(notification.chain_id);
488            }
489            Reason::NewRound { .. } => {
490                self.update_validators(&notification).await?;
491            }
492            Reason::NewBlock { hash, .. } => {
493                self.update_wallet(notification.chain_id).await?;
494                if listening_mode.is_full() {
495                    self.add_new_chains(*hash).await?;
496                    let publishers = self
497                        .update_event_subscriptions(notification.chain_id)
498                        .await?;
499                    if !publishers.is_empty() {
500                        self.listen_recursively(publishers).await?;
501                        self.maybe_notify_inbox_processing(notification.chain_id);
502                    }
503                }
504                self.process_new_events(notification.chain_id);
505            }
506            Reason::NewEvents { .. } => {
507                self.process_new_events(notification.chain_id);
508            }
509            Reason::BlockExecuted { .. } => {}
510        }
511        Self::sleep(self.config.delay_after_ms).await;
512        Ok(())
513    }
514
515    /// If any new chains were created by the given block, and we have a key pair for at
516    /// least one of their owners, add them to the wallet and start listening for
517    /// notifications. The preferred owner is assigned only when we hold a key pair for
518    /// exactly one of the chain's owners. (Fallback owners are ignored, as those would
519    /// have to monitor all chains anyway.)
520    async fn add_new_chains(&mut self, hash: CryptoHash) -> Result<(), Error> {
521        let block = CacheArc::unwrap_or_clone(
522            self.storage
523                .read_confirmed_block(hash)
524                .await?
525                .ok_or(chain_client::Error::MissingConfirmedBlock(hash))?,
526        )
527        .into_block();
528        let parent_chain_id = block.header.chain_id;
529        let blobs = block.created_blobs().into_iter();
530        let new_chains = blobs
531            .filter_map(|(blob_id, blob)| {
532                if blob_id.blob_type == BlobType::ChainDescription {
533                    let chain_desc: ChainDescription = bcs::from_bytes(blob.content().bytes())
534                        .expect("ChainDescription should deserialize correctly");
535                    Some((ChainId(blob_id.hash), chain_desc))
536                } else {
537                    None
538                }
539            })
540            .collect::<Vec<_>>();
541        if new_chains.is_empty() {
542            return Ok(());
543        }
544        let mut new_ids = BTreeMap::new();
545        let mut context_guard = self.context.lock().await;
546        for (new_chain_id, chain_desc) in new_chains {
547            let with_key = context_guard
548                .owners_with_key(chain_desc.config().ownership.all_owners().copied())
549                .await?;
550            if with_key.is_empty() {
551                continue;
552            }
553            let owner = if with_key.len() == 1 {
554                with_key.into_iter().next()
555            } else {
556                None
557            };
558            context_guard
559                .update_wallet_for_new_chain(
560                    new_chain_id,
561                    owner,
562                    block.header.timestamp,
563                    block.header.epoch,
564                )
565                .await?;
566            context_guard
567                .client()
568                .extend_chain_mode(new_chain_id, ListeningMode::FullChain);
569            new_ids.insert(new_chain_id, ListeningMode::FullChain);
570        }
571        // Re-process the parent chain's outboxes now that the new chains are tracked.
572        // This ensures cross-chain messages to newly created chains are delivered.
573        if !new_ids.is_empty() {
574            context_guard
575                .client()
576                .retry_pending_cross_chain_requests(parent_chain_id)
577                .await?;
578        }
579        drop(context_guard);
580        self.listen_recursively(new_ids).await?;
581        Ok(())
582    }
583
584    /// Notifies all chains subscribed to `chain_id` to process their inboxes.
585    fn process_new_events(&self, chain_id: ChainId) {
586        let Some(subscribers) = self.event_subscribers.get(&chain_id) else {
587            return;
588        };
589        for subscriber_id in subscribers {
590            self.maybe_notify_inbox_processing(*subscriber_id);
591        }
592    }
593
594    /// Starts listening for notifications about the given chains, and any chains that publish
595    /// event streams those chains are subscribed to.
596    async fn listen_recursively(
597        &mut self,
598        mut chain_ids: BTreeMap<ChainId, ListeningMode>,
599    ) -> Result<(), Error> {
600        while let Some((chain_id, listening_mode)) = chain_ids.pop_first() {
601            for (new_chain_id, new_listening_mode) in self.listen(chain_id, listening_mode).await? {
602                match chain_ids.entry(new_chain_id) {
603                    Entry::Vacant(vacant) => {
604                        vacant.insert(new_listening_mode);
605                    }
606                    Entry::Occupied(mut occupied) => {
607                        occupied.get_mut().extend(Some(new_listening_mode));
608                    }
609                }
610            }
611        }
612
613        Ok(())
614    }
615
616    /// Background task that syncs received certificates in small batches.
617    /// This discovers unacknowledged sender blocks gradually without overwhelming the system.
618    #[instrument(skip(context))]
619    async fn background_sync_received_certificates(
620        context: Arc<Mutex<C>>,
621        chain_id: ChainId,
622    ) -> Result<(), Error> {
623        info!("Starting background certificate sync");
624        let client = context.lock().await.make_chain_client(chain_id).await?;
625
626        Ok(client.find_received_certificates().await?)
627    }
628
629    /// Starts listening for notifications about the given chain.
630    ///
631    /// Returns all publishing chains, that we also need to listen to.
632    async fn listen(
633        &mut self,
634        chain_id: ChainId,
635        listening_mode: ListeningMode,
636    ) -> Result<BTreeMap<ChainId, ListeningMode>, Error> {
637        let context_guard = self.context.lock().await;
638        let existing_mode = context_guard.client().chain_mode(chain_id);
639        // If we already have a listener with a sufficient mode, nothing to do.
640        if self.listening.contains_key(&chain_id)
641            && existing_mode.as_ref().is_some_and(|m| *m >= listening_mode)
642        {
643            return Ok(BTreeMap::new());
644        }
645        // Extend the mode in the central map.
646        context_guard
647            .client()
648            .extend_chain_mode(chain_id, listening_mode);
649        drop(context_guard);
650
651        // Start background tasks to sync received certificates, if enabled.
652        let background_sync_task = self.start_background_sync(chain_id).await;
653        let client = self
654            .context
655            .lock()
656            .await
657            .make_chain_client(chain_id)
658            .await?;
659        let (listener, abort_handle, notification_stream) = client.listen().await?;
660        let listening_client = ListeningClient::new(
661            client,
662            abort_handle,
663            Task::spawn(listener.in_current_span()),
664            notification_stream,
665            background_sync_task,
666            &self.context,
667            &self.config,
668            &self.cancellation_token,
669        );
670        self.listening.insert(chain_id, listening_client);
671        let publishing_chains = self.update_event_subscriptions(chain_id).await?;
672        self.maybe_notify_inbox_processing(chain_id);
673        Ok(publishing_chains)
674    }
675
676    async fn start_background_sync(&mut self, chain_id: ChainId) -> Task<()> {
677        if !self.enable_background_sync
678            || !self
679                .context
680                .lock()
681                .await
682                .client()
683                .chain_mode(chain_id)
684                .is_some_and(|m| m.is_full())
685        {
686            return Task::ready(());
687        }
688
689        let context = Arc::clone(&self.context);
690        let interval = Duration::from_millis(self.config.background_sync_interval_ms);
691        Task::spawn(async move {
692            loop {
693                if let Err(e) =
694                    Self::background_sync_received_certificates(Arc::clone(&context), chain_id)
695                        .await
696                {
697                    warn!("Background sync failed for chain {chain_id}: {e}");
698                }
699                if interval.is_zero() {
700                    return;
701                }
702                linera_base::time::timer::sleep(interval).await;
703            }
704        })
705    }
706
707    /// Removes `chain_id` as a subscriber from every publisher, and returns the publishers that
708    /// no longer have any subscribers as a result.
709    fn remove_event_subscriber(&mut self, chain_id: ChainId) -> Vec<ChainId> {
710        let mut orphaned = Vec::new();
711        self.event_subscribers.retain(|publisher_id, subscribers| {
712            subscribers.remove(&chain_id);
713            if subscribers.is_empty() {
714                orphaned.push(*publisher_id);
715                false
716            } else {
717                true
718            }
719        });
720        orphaned
721    }
722
723    /// Stops listening to a publisher chain whose last subscriber went away, provided the chain is
724    /// only tracked because of event subscriptions (i.e. its mode is `EventsOnly`). Wallet chains
725    /// (`FullChain`/`FollowChain`) are left untouched, as is any chain we're not listening to.
726    async fn stop_tracking_publisher(&mut self, publisher_id: ChainId) {
727        let mode = self.context.lock().await.client().chain_mode(publisher_id);
728        if !matches!(mode, Some(ListeningMode::EventsOnly(_))) {
729            return;
730        }
731        let Some(listening_client) = self.listening.remove(&publisher_id) else {
732            return;
733        };
734        self.context
735            .lock()
736            .await
737            .client()
738            .remove_chain_mode(publisher_id);
739        listening_client.stop().await;
740        debug!(%publisher_id, "stopped tracking publisher chain after its last subscriber went away");
741    }
742
743    /// Updates the event subscribers map, and returns all publishing chains we need to listen to.
744    async fn update_event_subscriptions(
745        &mut self,
746        chain_id: ChainId,
747    ) -> Result<BTreeMap<ChainId, ListeningMode>, Error> {
748        let listening_client = self.listening.get_mut(&chain_id).expect("missing client");
749        if !listening_client.client.is_tracked() {
750            return Ok(BTreeMap::new());
751        }
752        let app_filter = listening_client
753            .client
754            .options()
755            .message_policy
756            .process_events_from_application_ids
757            .clone();
758        let publishing_chains: BTreeMap<_, _> = listening_client
759            .client
760            .event_stream_publishers()
761            .await?
762            .into_iter()
763            .filter_map(|(chain_id, streams)| {
764                let streams = if let Some(app_set) = &app_filter {
765                    streams
766                        .into_iter()
767                        .filter(|s| app_set.contains(&s.application_id))
768                        .collect::<BTreeSet<_>>()
769                } else {
770                    streams
771                };
772                if streams.is_empty() {
773                    None
774                } else {
775                    Some((chain_id, ListeningMode::EventsOnly(streams)))
776                }
777            })
778            .collect();
779        for publisher_id in publishing_chains.keys() {
780            self.event_subscribers
781                .entry(*publisher_id)
782                .or_default()
783                .insert(chain_id);
784        }
785        // Detect publishers this chain no longer subscribes to (e.g. an application called
786        // `unsubscribe_from_events`), and stop tracking any that were only tracked on its behalf.
787        let removed_publishers = self
788            .event_subscribers
789            .iter()
790            .filter(|(publisher_id, subscribers)| {
791                subscribers.contains(&chain_id) && !publishing_chains.contains_key(publisher_id)
792            })
793            .map(|(publisher_id, _)| *publisher_id)
794            .collect::<Vec<_>>();
795        for publisher_id in removed_publishers {
796            let orphaned = {
797                let Some(subscribers) = self.event_subscribers.get_mut(&publisher_id) else {
798                    continue;
799                };
800                subscribers.remove(&chain_id);
801                subscribers.is_empty()
802            };
803            if orphaned {
804                self.event_subscribers.remove(&publisher_id);
805                self.stop_tracking_publisher(publisher_id).await;
806            }
807        }
808        Ok(publishing_chains)
809    }
810
811    /// Returns the next notification to process, or a stop signal.
812    async fn next_action(&mut self) -> Result<Action, Error> {
813        loop {
814            let notification_futures = self
815                .listening
816                .values_mut()
817                .map(|client| {
818                    let stream = client.notification_stream.clone();
819                    Box::pin(async move { stream.lock().await.next().await })
820                })
821                .collect::<Vec<_>>();
822            futures::select! {
823                () = self.cancellation_token.cancelled().fuse() => {
824                    return Ok(Action::Stop);
825                }
826                command = self.command_receiver.recv().then(async |maybe_command| {
827                    if let Some(command) = maybe_command {
828                        command
829                    } else {
830                        std::future::pending().await
831                    }
832                }).fuse() => {
833                    match command {
834                        ListenerCommand::Listen(new_chains) => {
835                            debug!(?new_chains, "received command to listen to new chains");
836                            let listening_modes = self.update_wallet_for_listening(new_chains).await?;
837                            self.listen_recursively(listening_modes).await?;
838                        }
839                        ListenerCommand::StopListening(chains) => {
840                            debug!(?chains, "received command to stop listening to chains");
841                            for chain_id in chains {
842                                debug!(%chain_id, "stopping the listener for chain");
843                                let Some(listening_client) = self.listening.remove(&chain_id) else {
844                                    error!(%chain_id, "attempted to drop a non-existent listener");
845                                    continue;
846                                };
847                                let orphaned = self.remove_event_subscriber(chain_id);
848                                listening_client.stop().await;
849                                for publisher_id in orphaned {
850                                    self.stop_tracking_publisher(publisher_id).await;
851                                }
852                                if let Err(error) = self.context.lock().await.wallet().remove(chain_id).await {
853                                    error!(%error, %chain_id, "error removing a chain from the wallet");
854                                }
855                            }
856                        }
857                        ListenerCommand::SetMessagePolicy(policies) => {
858                            debug!(?policies, "received command to set message policies");
859                            for (chain_id, policy) in policies {
860                                let Some(listening_client) = self.listening.get_mut(&chain_id) else {
861                                    error!(
862                                        %chain_id,
863                                        "attempted to set the message policy of a non-existent \
864                                        listener"
865                                    );
866                                    continue;
867                                };
868                                listening_client.client.options_mut().message_policy = policy;
869                                listening_client.respawn_inbox_task(
870                                    &self.cancellation_token,
871                                    &self.context,
872                                    &self.config,
873                                );
874                            }
875                        }
876                    }
877                }
878                (maybe_notification, index, _) = future::select_all(notification_futures).fuse() => {
879                    let Some(notification) = maybe_notification else {
880                        let chain_id = *self.listening.keys().nth(index).unwrap();
881                        warn!("Notification stream for {chain_id} closed");
882                        let Some(listening_client) = self.listening.remove(&chain_id) else {
883                            error!(%chain_id, "attempted to drop a non-existent listener");
884                            continue;
885                        };
886                        let orphaned = self.remove_event_subscriber(chain_id);
887                        listening_client.stop().await;
888                        for publisher_id in orphaned {
889                            self.stop_tracking_publisher(publisher_id).await;
890                        }
891                        continue;
892                    };
893                    return Ok(Action::Notification(notification));
894                }
895            }
896        }
897    }
898
899    /// Updates the validators about the chain.
900    async fn update_validators(&self, notification: &Notification) -> Result<(), Error> {
901        let chain_id = notification.chain_id;
902        let listening_client = self.listening.get(&chain_id).expect("missing client");
903        let latest_block = if let Reason::NewBlock { hash, .. } = &notification.reason {
904            listening_client.client.read_certificate(*hash).await.ok()
905        } else {
906            None
907        };
908        if let Err(error) = listening_client
909            .client
910            .update_validators(None, latest_block)
911            .await
912        {
913            warn!(
914                "Failed to update validators about the local chain after \
915                 receiving {notification:?} with error: {error:?}"
916            );
917        }
918        Ok(())
919    }
920
921    /// Updates the wallet based on the client for this chain.
922    async fn update_wallet(&self, chain_id: ChainId) -> Result<(), Error> {
923        let client = &self
924            .listening
925            .get(&chain_id)
926            .expect("missing client")
927            .client;
928        self.context.lock().await.update_wallet(client).await?;
929        Ok(())
930    }
931
932    /// Updates the wallet with the set of chains we're supposed to start listening to,
933    /// and returns the appropriate listening modes based on whether we have the private
934    /// keys corresponding to the given chains' owners.
935    async fn update_wallet_for_listening(
936        &self,
937        new_chains: BTreeMap<ChainId, Option<AccountOwner>>,
938    ) -> Result<BTreeMap<ChainId, ListeningMode>, Error> {
939        let mut chains = BTreeMap::new();
940        let context_guard = self.context.lock().await;
941        for (chain_id, owner) in new_chains {
942            if let Some(owner) = owner {
943                if context_guard
944                    .client()
945                    .signer()
946                    .contains_key(&owner)
947                    .await
948                    .map_err(chain_client::Error::signer_failure)?
949                {
950                    // Try to modify existing chain entry, setting the owner.
951                    let modified = context_guard
952                        .wallet()
953                        .modify(chain_id, |chain| chain.owner = Some(owner))
954                        .await
955                        .map_err(error::Error::wallet)?;
956                    // If the chain didn't exist, insert a new entry.
957                    if modified.is_none() {
958                        let chain_description = context_guard
959                            .client()
960                            .get_chain_description(chain_id)
961                            .await?;
962                        let timestamp = chain_description.timestamp();
963                        let epoch = chain_description.config().epoch;
964                        context_guard
965                            .wallet()
966                            .insert(
967                                chain_id,
968                                linera_core::wallet::Chain {
969                                    owner: Some(owner),
970                                    timestamp,
971                                    epoch: Some(epoch),
972                                    ..Default::default()
973                                },
974                            )
975                            .await
976                            .map_err(error::Error::wallet)?;
977                    }
978
979                    chains.insert(chain_id, ListeningMode::FullChain);
980                }
981            } else {
982                chains.insert(chain_id, ListeningMode::FollowChain);
983            }
984        }
985        Ok(chains)
986    }
987
988    /// Signals the per-chain inbox processing task to wake up and process the inbox.
989    fn maybe_notify_inbox_processing(&self, chain_id: ChainId) {
990        if let Some(listening_client) = self.listening.get(&chain_id) {
991            listening_client.inbox_notify.notify_one();
992        }
993    }
994
995    /// Sleeps for the given number of milliseconds, if greater than 0.
996    async fn sleep(delay_ms: u64) {
997        if delay_ms > 0 {
998            linera_base::time::timer::sleep(Duration::from_millis(delay_ms)).await;
999        }
1000    }
1001}
1002
1003/// Per-chain inbox processing loop. Runs as a long-lived tokio task. Wakes on
1004/// `inbox_notify` signals and processes the inbox, handling round-leader timeouts
1005/// internally. Multiple notifications while busy collapse into a single permit.
1006async fn inbox_processing_loop<C: ClientContext>(
1007    client: ContextChainClient<C>,
1008    context: Arc<Mutex<C>>,
1009    config: Arc<ChainListenerConfig>,
1010    inbox_notify: Arc<Notify>,
1011    cancellation_token: CancellationToken,
1012) {
1013    let chain_id = client.chain_id();
1014    loop {
1015        futures::select! {
1016            () = cancellation_token.cancelled().fuse() => break,
1017            () = inbox_notify.notified().fuse() => {
1018                if config.skip_process_inbox {
1019                    debug!("Not processing inbox for {chain_id:.8} due to listener configuration");
1020                    continue;
1021                }
1022                if !client.is_tracked() {
1023                    debug!("Not processing inbox for non-tracked chain {chain_id:.8}");
1024                    continue;
1025                }
1026                if client.preferred_owner().is_none() {
1027                    debug!("Not processing inbox for follow-only chain {chain_id:.8}");
1028                    continue;
1029                }
1030                debug!("Processing inbox for {chain_id:.8}");
1031
1032                // Inner loop handles round-leader timeouts: if we can't produce a block
1033                // because we're not the leader, sleep until the timeout then retry.
1034                // A new notification or cancellation can interrupt the sleep.
1035                loop {
1036                    match client.process_inbox_without_prepare().await {
1037                        Err(chain_client::Error::CannotFindKeyForChain(chain_id)) => {
1038                            debug!(%chain_id, "Cannot find key for chain");
1039                            break;
1040                        }
1041                        Err(error) => {
1042                            warn!(%error, "Failed to process inbox");
1043                            break;
1044                        }
1045                        Ok((certs, None)) => {
1046                            if certs.is_empty() {
1047                                debug!(%chain_id, "done processing inbox: no blocks created");
1048                            } else {
1049                                info!(
1050                                    %chain_id,
1051                                    created_block_count = %certs.len(),
1052                                    "done processing inbox",
1053                                );
1054                            }
1055                            break;
1056                        }
1057                        Ok((certs, Some(new_timeout))) => {
1058                            info!(
1059                                %chain_id,
1060                                created_block_count = %certs.len(),
1061                                timeout = %new_timeout,
1062                                "waiting for round timeout before continuing to process the inbox",
1063                            );
1064                            let delta = new_timeout.timestamp.delta_since(Timestamp::now());
1065                            if delta > TimeDelta::ZERO {
1066                                futures::select! {
1067                                    () = cancellation_token.cancelled().fuse() => return,
1068                                    () = linera_base::time::timer::sleep(delta.as_duration()).fuse() => {},
1069                                    () = inbox_notify.notified().fuse() => {},
1070                                }
1071                            }
1072                        }
1073                    }
1074                }
1075
1076                if let Err(error) = context.lock().await.update_wallet(&client).await {
1077                    warn!(%error, "Failed to update wallet after inbox processing");
1078                }
1079            }
1080        }
1081    }
1082}
1083
1084enum Action {
1085    Notification(Notification),
1086    Stop,
1087}