1use 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#[derive(Debug, Clone, clap::Args, serde::Serialize, serde::Deserialize, tsify::Tsify)]
37#[serde(rename_all = "camelCase")]
38pub struct ChainListenerConfig {
39 #[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 #[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 #[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 #[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
80fn default_background_sync_interval_ms() -> u64 {
82 900_000
83}
84
85impl 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#[cfg_attr(not(web), trait_variant::make(Send + Sync))]
104#[allow(async_fn_in_trait)]
105pub trait ClientContext {
106 type Environment: linera_core::Environment;
108
109 fn wallet(&self) -> &<Self::Environment as linera_core::Environment>::Wallet;
111
112 fn storage(&self) -> &<Self::Environment as linera_core::Environment>::Storage;
114
115 fn client(&self) -> &Arc<linera_core::client::Client<Self::Environment>>;
117
118 fn admin_chain_id(&self) -> ChainId {
120 self.client().admin_chain_id()
121 }
122
123 #[cfg(not(web))]
125 fn timing_sender(
126 &self,
127 ) -> Option<tokio::sync::mpsc::UnboundedSender<(u64, linera_core::client::TimingType)>>;
128
129 #[cfg(web)]
131 fn timing_sender(
132 &self,
133 ) -> Option<tokio::sync::mpsc::UnboundedSender<(u64, linera_core::client::TimingType)>> {
134 None
135 }
136
137 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 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 async fn update_wallet(&mut self, client: &ContextChainClient<Self>) -> Result<(), Error>;
174}
175
176#[allow(async_fn_in_trait)]
178pub trait ClientContextExt: ClientContext {
179 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 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 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 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
254struct ListeningClient<C: ClientContext> {
260 client: ContextChainClient<C>,
262 abort_handle: AbortOnDrop,
264 listener: Task<()>,
266 notification_stream: Arc<Mutex<NotificationStream>>,
268 background_sync: Task<()>,
270 inbox_notify: Arc<Notify>,
272 inbox_task: Task<()>,
274 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)] notification_stream: Arc::new(Mutex::new(notification_stream)),
300 background_sync,
301 inbox_notify,
302 inbox_task,
303 inbox_cancellation,
304 }
305 }
306
307 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 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
355pub enum ListenerCommand {
357 Listen(BTreeMap<ChainId, Option<AccountOwner>>),
360 StopListening(BTreeSet<ChainId>),
362 SetMessagePolicy(BTreeMap<ChainId, MessagePolicy>),
364}
365
366pub 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 event_subscribers: BTreeMap<ChainId, BTreeSet<ChainId>>,
377 command_receiver: UnboundedReceiver<ListenerCommand>,
379 enable_background_sync: bool,
381}
382
383impl<C: ClientContext + 'static> ChainListener<C> {
384 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 #[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 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 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(¬ification.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(¬ification.reason) {
479 debug!(
480 reason = ?notification.reason,
481 "ChainListener: ignoring notification due to listening mode"
482 );
483 return Ok(());
484 }
485 match ¬ification.reason {
486 Reason::NewIncomingBundle { .. } => {
487 self.maybe_notify_inbox_processing(notification.chain_id);
488 }
489 Reason::NewRound { .. } => {
490 self.update_validators(¬ification).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 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 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 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 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 #[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 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 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 context_guard
647 .client()
648 .extend_chain_mode(chain_id, listening_mode);
649 drop(context_guard);
650
651 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 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 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 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 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 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 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, .. } = ¬ification.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 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 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 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 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 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 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
1003async 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 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}