Skip to main content

linera_core/client/chain_client/
mod.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4mod state;
5use std::{
6    collections::{
7        hash_map::{self, DefaultHasher},
8        BTreeMap, BTreeSet, HashMap, HashSet,
9    },
10    convert::Infallible,
11    hash::{Hash as _, Hasher as _},
12    iter,
13    sync::{
14        atomic::{AtomicU64, Ordering},
15        Arc,
16    },
17};
18
19use custom_debug_derive::Debug;
20use futures::{
21    future::{self, Either, FusedFuture, Future},
22    stream::{self, AbortHandle, FusedStream, FuturesUnordered, StreamExt, TryStreamExt},
23};
24#[cfg(with_metrics)]
25use linera_base::prometheus_util::MeasureLatency as _;
26use linera_base::{
27    abi::Abi,
28    crypto::{signer, CryptoHash, Signer, ValidatorPublicKey},
29    data_types::{
30        Amount, ApplicationDescription, ApplicationPermissions, ArithmeticError, Blob, BlobContent,
31        BlockHeight, ChainDescription, Epoch, MessagePolicy, Round, TimeDelta, Timestamp,
32    },
33    ensure,
34    identifiers::{
35        Account, AccountOwner, ApplicationId, BlobId, BlobType, ChainId, EventId, IndexAndEvent,
36        ModuleId, StreamId,
37    },
38    ownership::{ChainOwnership, TimeoutConfig},
39    time::{Duration, Instant},
40};
41#[cfg(not(target_arch = "wasm32"))]
42use linera_base::{data_types::Bytecode, vm::VmRuntime};
43use linera_chain::{
44    data_types::{
45        BlockProposal, BundleExecutionPolicy, BundleFailurePolicy, ChainAndHeight, IncomingBundle,
46        ProposedBlock, Transaction,
47    },
48    manager::LockingBlock,
49    types::{
50        Block, ConfirmedBlock, ConfirmedBlockCertificate, Timeout, TimeoutCertificate,
51        ValidatedBlock,
52    },
53    ChainError, ChainExecutionContext,
54};
55use linera_execution::{
56    committee::Committee,
57    system::{
58        AdminOperation, OpenChainConfig, SystemOperation, EPOCH_STREAM_NAME,
59        REMOVED_EPOCH_STREAM_NAME,
60    },
61    ExecutionError, Operation, Query, QueryOutcome,
62};
63use linera_storage::{Arc as CacheArc, Clock as _, Storage as _};
64use linera_views::ViewError;
65use serde::Serialize;
66pub use state::State;
67use thiserror::Error;
68use tokio::sync::mpsc;
69use tokio_stream::wrappers::UnboundedReceiverStream;
70use tracing::{debug, error, info, instrument, trace, warn, Instrument as _};
71
72use super::{
73    received_log::ReceivedLogs, validator_trackers::ValidatorTrackers, AbortOnDrop, Client,
74    ListeningMode, PendingProposal, TimingType,
75};
76use crate::{
77    data_types::{ChainInfo, ChainInfoQuery, ClientOutcome, RoundTimeout},
78    environment::Environment,
79    local_node::{LocalNodeClient, LocalNodeError},
80    node::{
81        CrossChainMessageDelivery, NodeError, NotificationStream, ValidatorNode,
82        ValidatorNodeProvider as _,
83    },
84    remote_node::RemoteNode,
85    updater::{communicate_with_quorum, CommunicateAction, CommunicationError},
86    worker::{Notification, Reason, WorkerError},
87    MaybeSendBoxFuture,
88};
89
90/// Number of certificates above which `find_received_certificates` reports its start,
91/// progress, and completion at `info` level. Smaller syncs stay quiet outside `debug`/`trace`
92/// logging. Counted in certificates, not in received-log entries: the entry total is summed
93/// over validators, so the two differ and the log line reports both.
94const LARGE_RECEIVED_CERTIFICATE_SYNC_THRESHOLD: usize = 50_000;
95
96/// Number of certificates processed between two `info`-level progress messages during a
97/// large sync of received certificates.
98const RECEIVED_CERTIFICATE_SYNC_PROGRESS_INTERVAL: usize = 100_000;
99
100/// Options that configure the behavior of a [`ChainClient`].
101#[derive(Debug, Clone)]
102pub struct Options {
103    /// Maximum number of pending message bundles processed at a time in a block.
104    pub max_pending_message_bundles: usize,
105    /// Maximum number of message bundles to discard from a block proposal due to block limit
106    /// errors before discarding all remaining bundles.
107    ///
108    /// Discarded bundles can be retried in the next block.
109    pub max_block_limit_errors: u32,
110    /// Time budget for staging message bundles. When set, limits bundle execution by
111    /// wall-clock time, in addition to the count limit from `max_pending_message_bundles`.
112    pub staging_bundles_time_budget: Option<Duration>,
113    /// The policy for automatically handling incoming messages.
114    pub message_policy: MessagePolicy,
115    /// Chain IDs whose incoming bundles should be processed first when proposing a block.
116    pub priority_bundle_origins: HashSet<ChainId>,
117    /// Whether to block on cross-chain message delivery.
118    pub cross_chain_message_delivery: CrossChainMessageDelivery,
119    /// An additional delay, after reaching a quorum, to wait for additional validator signatures,
120    /// as a fraction of time taken to reach quorum.
121    pub quorum_grace_period: f64,
122    /// The delay when downloading a blob, after which we try a second validator.
123    pub blob_download_hedge_delay: Duration,
124    /// The delay when downloading a batch of certificates, after which we try a second validator.
125    pub certificate_batch_download_hedge_delay: Duration,
126    /// Maximum number of certificates that we download at a time from one validator when
127    /// synchronizing one of our chains.
128    pub certificate_download_batch_size: u64,
129    /// Maximum number of certificates read from local storage and uploaded to a validator
130    /// at a time when synchronizing a chain.
131    pub certificate_upload_batch_size: usize,
132    /// Maximum number of sender certificates we try to download and receive in one go
133    /// when syncing sender chains.
134    pub sender_certificate_download_batch_size: usize,
135    /// Maximum number of certificate batches downloaded concurrently during chain sync.
136    pub max_concurrent_batch_downloads: usize,
137    /// Maximum number of tasks that can be joined concurrently using buffer_unordered.
138    pub max_joined_tasks: usize,
139    /// Whether to allow creating blocks in the fast round. Fast blocks have lower latency but
140    /// must be used carefully so that there are never any conflicting fast block proposals.
141    pub allow_fast_blocks: bool,
142    /// Initial probe interval for the notification circuit breaker. When a validator's
143    /// notification stream exhausts retries, the circuit breaker waits this long before
144    /// probing again. Doubles on each failed probe.
145    pub notification_circuit_breaker_initial_probe_interval: Duration,
146    /// Maximum probe interval for the notification circuit breaker. The probe interval
147    /// doubles on each failure but is capped at this value.
148    pub notification_circuit_breaker_max_probe_interval: Duration,
149    /// Maximum number of event stream IDs to include in a single `PreviousEventBlocks`
150    /// request. Larger sets are split into multiple requests.
151    pub max_event_stream_queries: usize,
152}
153
154/// A validator's notification stream: its abort handle, plus when it began serving.
155pub(super) struct StreamHandle {
156    pub(super) abort: AbortHandle,
157    /// Microseconds at which `subscribe` *and* the initial sync both returned, or
158    /// [`NOT_SERVING`] while either is still in flight. Nothing polls the notification
159    /// stream until the sync returns, so "subscribe resolved" is too weak to mean healthy.
160    pub(super) serving_since: Arc<AtomicU64>,
161}
162
163impl StreamHandle {
164    /// How long the stream has been serving notifications, or `None` if it never started.
165    fn serving_for(&self, now: Timestamp) -> Option<Duration> {
166        match self.serving_since.load(Ordering::Relaxed) {
167            NOT_SERVING => None,
168            micros => Some(now.duration_since(Timestamp::from(micros))),
169        }
170    }
171}
172
173pub(super) struct CircuitBreakerState {
174    pub(super) next_probe_at: Timestamp,
175    pub(super) probe_interval: Duration,
176    /// Whether the stream has actually failed, as opposed to this being the deadline
177    /// armed when it was launched. Only a breaker that tripped reports a recovery.
178    pub(super) tripped: bool,
179}
180
181impl CircuitBreakerState {
182    /// The deadline every launch arms, before anything is known about the stream.
183    fn launched(now: Timestamp, probe_interval: Duration, spread: Duration) -> Self {
184        Self::armed(now, probe_interval, spread, false)
185    }
186
187    /// A breaker for a stream that has failed.
188    fn failed(now: Timestamp, probe_interval: Duration, spread: Duration) -> Self {
189        Self::armed(now, probe_interval, spread, true)
190    }
191
192    /// A breaker for a long-lived stream that ended — routine, so it does not trip.
193    ///
194    /// Tripping here would report a recovery on the next successful probe for something
195    /// this arm has just classified as healthy: one rolling validator restart would emit a
196    /// spurious "recovered" line per chain per validator.
197    fn churned(now: Timestamp, probe_interval: Duration, spread: Duration) -> Self {
198        Self::armed(now, probe_interval, spread, false)
199    }
200
201    fn armed(now: Timestamp, probe_interval: Duration, spread: Duration, tripped: bool) -> Self {
202        CircuitBreakerState {
203            next_probe_at: now.saturating_add(TimeDelta::from_duration(probe_interval + spread)),
204            probe_interval,
205            tripped,
206        }
207    }
208
209    /// Doubles the interval, up to `max`, and re-arms. Only a failure escalates, so this
210    /// also trips the breaker.
211    fn escalate(&mut self, now: Timestamp, max: Duration, spread: Duration) {
212        self.probe_interval = self.probe_interval.saturating_mul(2).min(max);
213        self.tripped = true;
214        self.arm(now, spread);
215    }
216
217    /// Re-arms one interval out, plus a per-validator offset so that a fleet-wide stream
218    /// loss does not re-converge every chain's probes on the same instant at every
219    /// doubling.
220    fn arm(&mut self, now: Timestamp, spread: Duration) {
221        self.next_probe_at =
222            now.saturating_add(TimeDelta::from_duration(self.probe_interval + spread));
223    }
224}
225
226/// Sentinel in [`StreamHandle::serving_since`] for a stream that has not started serving.
227pub(super) const NOT_SERVING: u64 = u64::MAX;
228
229/// Floor for the circuit-breaker probe interval.
230///
231/// The flags are validated where they are parsed, so this is a backstop for programmatic
232/// [`Options`], not a guard against arithmetic: a sub-second probe is a storm, since each
233/// one is a `subscribe` plus a full `synchronize_chain_state_from`.
234const MIN_PROBE_INTERVAL: Duration = Duration::from_secs(1);
235
236/// Fraction of the probe interval used as the per-validator spread, as a divisor.
237const PROBE_SPREAD_DIVISOR: u32 = 8;
238
239/// How much longer a stream's FIRST launch gets before it is abandoned as stalled.
240///
241/// A re-probe and a first sync are different quantities. "How often to retry a validator"
242/// is the probe interval; "how long an initial `synchronize_chain_state_from` may take" is
243/// unbounded by nature — a chain tens of thousands of certificates behind legitimately
244/// needs longer. Sharing one deadline aborts every stream at once on exactly the chains
245/// furthest behind, throwing away the tail of each attempt just as the backoff doubles.
246const FIRST_LAUNCH_GRACE: u32 = 4;
247
248/// A fingerprint of the chain manager's observable consensus state, used to detect
249/// whether a per-validator updater absorbed new info during a proposal attempt.
250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251struct ConsensusStateSnapshot {
252    next_block_height: BlockHeight,
253    current_round: Round,
254    lock_round: Option<Round>,
255    timeout_round: Option<Round>,
256}
257
258#[cfg(with_testing)]
259impl Options {
260    /// Returns a default set of options for use in tests.
261    pub fn test_default() -> Self {
262        use super::{
263            DEFAULT_CERTIFICATE_DOWNLOAD_BATCH_SIZE, DEFAULT_CERTIFICATE_UPLOAD_BATCH_SIZE,
264            DEFAULT_MAX_CONCURRENT_BATCH_DOWNLOADS, DEFAULT_MAX_EVENT_STREAM_QUERIES,
265            DEFAULT_SENDER_CERTIFICATE_DOWNLOAD_BATCH_SIZE,
266        };
267        use crate::DEFAULT_QUORUM_GRACE_PERIOD;
268
269        Options {
270            max_pending_message_bundles: 10,
271            max_block_limit_errors: 3,
272            staging_bundles_time_budget: None,
273            message_policy: MessagePolicy::default(),
274            priority_bundle_origins: HashSet::new(),
275            cross_chain_message_delivery: CrossChainMessageDelivery::NonBlocking,
276            quorum_grace_period: DEFAULT_QUORUM_GRACE_PERIOD,
277            blob_download_hedge_delay: Duration::from_secs(1),
278            certificate_batch_download_hedge_delay: Duration::from_secs(1),
279            certificate_download_batch_size: DEFAULT_CERTIFICATE_DOWNLOAD_BATCH_SIZE,
280            certificate_upload_batch_size: DEFAULT_CERTIFICATE_UPLOAD_BATCH_SIZE,
281            sender_certificate_download_batch_size: DEFAULT_SENDER_CERTIFICATE_DOWNLOAD_BATCH_SIZE,
282            max_concurrent_batch_downloads: DEFAULT_MAX_CONCURRENT_BATCH_DOWNLOADS,
283            max_joined_tasks: 100,
284            allow_fast_blocks: false,
285            notification_circuit_breaker_initial_probe_interval: Duration::from_secs(300),
286            notification_circuit_breaker_max_probe_interval: Duration::from_secs(3600),
287            max_event_stream_queries: DEFAULT_MAX_EVENT_STREAM_QUERIES,
288        }
289    }
290}
291
292impl Options {
293    /// Builds the [`BundleExecutionPolicy`] based on the client options.
294    pub fn bundle_execution_policy(&self) -> BundleExecutionPolicy {
295        BundleExecutionPolicy {
296            on_failure: BundleFailurePolicy::AutoRetry {
297                max_failures: self.max_block_limit_errors,
298                never_reject_application_ids: Arc::new(
299                    self.message_policy.never_reject_application_ids.clone(),
300                ),
301            },
302            time_budget: self.staging_bundles_time_budget,
303        }
304    }
305}
306
307/// Client to operate a chain by interacting with validators and the given local storage
308/// implementation.
309/// * The chain being operated is called the "local chain" or just the "chain".
310/// * As a rule, operations are considered successful (and communication may stop) when
311///   they succeeded in gathering a quorum of responses.
312#[derive(Debug)]
313pub struct ChainClient<Env: Environment> {
314    /// The Linera [`Client`] that manages operations for this chain client.
315    #[debug(skip)]
316    pub(crate) client: Arc<Client<Env>>,
317    /// The off-chain chain ID.
318    chain_id: ChainId,
319    /// The client options.
320    #[debug(skip)]
321    options: Options,
322    /// The preferred owner of the chain used to sign proposals.
323    /// `None` if we cannot propose on this chain.
324    preferred_owner: Option<AccountOwner>,
325    /// The next block height as read from the wallet.
326    initial_next_block_height: BlockHeight,
327    /// The last block hash as read from the wallet.
328    initial_block_hash: Option<CryptoHash>,
329    /// Optional timing sender for benchmarking.
330    timing_sender: Option<mpsc::UnboundedSender<(u64, TimingType)>>,
331    /// Sender chain IDs whose bundles were discarded due to the never-reject policy.
332    /// These origins are excluded from `process_inbox` until the client is restarted.
333    skipped_origins: Arc<papaya::HashSet<ChainId>>,
334}
335
336impl<Env: Environment> Clone for ChainClient<Env> {
337    fn clone(&self) -> Self {
338        Self {
339            client: self.client.clone(),
340            chain_id: self.chain_id,
341            options: self.options.clone(),
342            preferred_owner: self.preferred_owner,
343            initial_next_block_height: self.initial_next_block_height,
344            initial_block_hash: self.initial_block_hash,
345            timing_sender: self.timing_sender.clone(),
346            skipped_origins: self.skipped_origins.clone(),
347        }
348    }
349}
350
351/// Error type for [`ChainClient`].
352#[derive(Debug, Error, strum::IntoStaticStr)]
353#[allow(missing_docs)]
354pub enum Error {
355    #[error("Local node operation failed: {0}")]
356    LocalNodeError(#[from] LocalNodeError),
357
358    #[error("Remote node operation failed: {0}")]
359    RemoteNodeError(#[from] NodeError),
360
361    /// A validator reported state ahead of the local node for this chain. The caller may pull
362    /// the missing state from that validator, then retry or surface the carried error.
363    #[error("The local node is lagging behind a validator on chain {chain_id}: {error}")]
364    LocalNodeLagging {
365        chain_id: ChainId,
366        error: Box<NodeError>,
367    },
368
369    #[error(transparent)]
370    ArithmeticError(#[from] ArithmeticError),
371
372    #[error("Missing certificates: {0:?}")]
373    ReadCertificatesError(Vec<CryptoHash>),
374
375    #[error("Missing confirmed block: {0:?}")]
376    MissingConfirmedBlock(CryptoHash),
377
378    #[error("JSON (de)serialization error: {0}")]
379    JsonError(#[from] serde_json::Error),
380
381    #[error("Chain operation failed: {0}")]
382    ChainError(#[from] ChainError),
383
384    #[error(transparent)]
385    CommunicationError(#[from] CommunicationError<NodeError>),
386
387    #[error("Internal error within chain client: {0}")]
388    InternalError(&'static str),
389
390    #[error(
391        "Cannot accept a certificate from an unknown committee in the future. \
392         Please synchronize the local view of the admin chain"
393    )]
394    CommitteeSynchronizationError,
395
396    #[error("The local node is behind the trusted state in wallet and needs synchronization with validators")]
397    WalletSynchronizationError,
398
399    #[error("The state of the client is incompatible with the proposed block: {0}")]
400    BlockProposalError(&'static str),
401
402    #[error(
403        "Cannot accept a certificate from a committee that was retired. \
404         Try a newer certificate from the same origin"
405    )]
406    CommitteeDeprecationError,
407
408    #[error("Protocol error within chain client: {0}")]
409    ProtocolError(&'static str),
410
411    #[error("Signer doesn't have key to sign for chain {0}")]
412    CannotFindKeyForChain(ChainId),
413
414    #[error("client is not configured to propose on chain {0}")]
415    NoAccountKeyConfigured(ChainId),
416
417    #[error("The chain client isn't owner on chain {0}")]
418    NotAnOwner(ChainId),
419
420    #[error(transparent)]
421    ViewError(#[from] ViewError),
422
423    #[error(
424        "Failed to download certificates and update local node to the next height \
425         {target_next_block_height} of chain {chain_id}"
426    )]
427    CannotDownloadCertificates {
428        chain_id: ChainId,
429        target_next_block_height: BlockHeight,
430    },
431
432    #[error("No validator provided a usable certificate registering blob {0}")]
433    CannotDownloadBlob(BlobId),
434
435    #[error(transparent)]
436    BcsError(#[from] bcs::Error),
437
438    #[error(
439        "Unexpected quorum: validators voted for block hash {hash} in {round}, \
440         expected block hash {expected_hash} in {expected_round}"
441    )]
442    UnexpectedQuorum {
443        hash: CryptoHash,
444        round: Round,
445        expected_hash: CryptoHash,
446        expected_round: Round,
447    },
448
449    #[error("signer error: {0}")]
450    Signer(#[source] Box<dyn signer::Error>),
451
452    #[error("Cannot revoke the current epoch {0}")]
453    CannotRevokeCurrentEpoch(Epoch),
454
455    #[error("Epoch is already revoked")]
456    EpochAlreadyRevoked,
457
458    #[error("Failed to download missing sender blocks from chain {chain_id} at height {height}")]
459    CannotDownloadMissingSenderBlock {
460        chain_id: ChainId,
461        height: BlockHeight,
462    },
463
464    #[error(
465        "A different block was already committed at this height. \
466         The committed certificate hash is {0}"
467    )]
468    Conflict(CryptoHash),
469
470    #[error(
471        "Execution outcome mismatch: AutoRetry and committed execution produced \
472         different outcomes for the same block"
473    )]
474    ExecutionOutcomeMismatch,
475}
476
477impl From<Infallible> for Error {
478    fn from(infallible: Infallible) -> Self {
479        match infallible {}
480    }
481}
482
483impl Error {
484    /// Wraps a signer error into a [`Error::Signer`] variant.
485    pub fn signer_failure(err: impl signer::Error + 'static) -> Self {
486        Self::Signer(Box::new(err))
487    }
488
489    /// Returns the qualified error variant name for the `error_type` metric label,
490    /// delegating to the wrapped error's `error_type()` so the underlying worker or
491    /// chain error name is surfaced rather than just the outer variant.
492    pub fn error_type(&self) -> String {
493        match self {
494            Error::LocalNodeError(local_node_error) => local_node_error.error_type(),
495            Error::ChainError(chain_error) => chain_error.error_type(),
496            other => {
497                let variant: &'static str = other.into();
498                format!("ChainClientError::{variant}")
499            }
500        }
501    }
502}
503
504impl<Env: Environment> ChainClient<Env> {
505    /// Creates a new [`ChainClient`] for the given chain.
506    pub fn new(
507        client: Arc<Client<Env>>,
508        chain_id: ChainId,
509        options: Options,
510        initial_block_hash: Option<CryptoHash>,
511        initial_next_block_height: BlockHeight,
512        preferred_owner: Option<AccountOwner>,
513        timing_sender: Option<mpsc::UnboundedSender<(u64, TimingType)>>,
514    ) -> Self {
515        ChainClient {
516            client,
517            chain_id,
518            options,
519            preferred_owner,
520            initial_block_hash,
521            initial_next_block_height,
522            timing_sender,
523            skipped_origins: Arc::new(papaya::HashSet::new()),
524        }
525    }
526
527    /// Returns whether this chain is in follow-only mode.
528    pub fn is_follow_only(&self) -> bool {
529        self.client.is_chain_follow_only(self.chain_id)
530    }
531
532    /// Returns the proposal mutex for this chain.
533    ///
534    /// The mutex serializes block proposals and holds the pending proposal (if any).
535    #[instrument(level = "trace", skip(self))]
536    fn proposal_mutex(&self) -> Arc<tokio::sync::Mutex<Option<PendingProposal>>> {
537        self.client
538            .chains
539            .pin()
540            .get(&self.chain_id)
541            .expect("Chain client constructed for invalid chain")
542            .proposal_mutex()
543    }
544
545    /// Returns the pending proposal, if any.
546    #[instrument(level = "trace", skip(self))]
547    pub async fn pending_proposal(&self) -> Option<PendingProposal> {
548        self.proposal_mutex().lock().await.clone()
549    }
550
551    /// Gets a reference to the client's signer instance.
552    #[instrument(level = "trace", skip(self))]
553    pub fn signer(&self) -> &impl Signer {
554        self.client.signer()
555    }
556
557    /// Returns whether the signer has a key for the given owner.
558    pub async fn has_key_for(&self, owner: &AccountOwner) -> Result<bool, Error> {
559        self.client.has_key_for(owner).await
560    }
561
562    /// Gets a mutable reference to the per-`ChainClient` options.
563    #[instrument(level = "trace", skip(self))]
564    pub fn options_mut(&mut self) -> &mut Options {
565        &mut self.options
566    }
567
568    /// Gets a reference to the per-`ChainClient` options.
569    #[instrument(level = "trace", skip(self))]
570    pub fn options(&self) -> &Options {
571        &self.options
572    }
573
574    /// Gets the ID of the associated chain.
575    #[instrument(level = "trace", skip(self))]
576    pub fn chain_id(&self) -> ChainId {
577        self.chain_id
578    }
579
580    /// Gets a clone of the timing sender for benchmarking.
581    pub fn timing_sender(&self) -> Option<mpsc::UnboundedSender<(u64, TimingType)>> {
582        self.timing_sender.clone()
583    }
584
585    /// Gets the ID of the admin chain.
586    #[instrument(level = "trace", skip(self))]
587    pub fn admin_chain_id(&self) -> ChainId {
588        self.client.admin_chain_id
589    }
590
591    /// Gets the currently preferred owner for signing the blocks.
592    #[instrument(level = "trace", skip(self))]
593    pub fn preferred_owner(&self) -> Option<AccountOwner> {
594        self.preferred_owner
595    }
596
597    /// Sets the new, preferred owner for signing the blocks.
598    #[instrument(level = "trace", skip(self))]
599    pub fn set_preferred_owner(&mut self, preferred_owner: AccountOwner) {
600        self.preferred_owner = Some(preferred_owner);
601    }
602
603    /// Obtains a `ChainStateView` for this client's chain.
604    #[instrument(level = "trace")]
605    pub async fn chain_state_view(
606        &self,
607    ) -> Result<crate::worker::ChainStateViewReadGuard<Env::Storage>, LocalNodeError> {
608        self.client.local_node.chain_state_view(self.chain_id).await
609    }
610
611    /// Returns chain IDs that this chain subscribes to.
612    #[instrument(level = "trace", skip(self))]
613    pub async fn event_stream_publishers(
614        &self,
615    ) -> Result<BTreeMap<ChainId, BTreeSet<StreamId>>, LocalNodeError> {
616        let subscriptions = self
617            .client
618            .local_node
619            .get_event_subscriptions(self.chain_id)
620            .await?;
621        let mut publishers = subscriptions.into_iter().fold(
622            BTreeMap::<ChainId, BTreeSet<StreamId>>::new(),
623            |mut map, ((chain_id, stream_id), _)| {
624                map.entry(chain_id).or_default().insert(stream_id);
625                map
626            },
627        );
628        if self.chain_id != self.client.admin_chain_id {
629            publishers.insert(
630                self.client.admin_chain_id,
631                vec![
632                    StreamId::system(EPOCH_STREAM_NAME),
633                    StreamId::system(REMOVED_EPOCH_STREAM_NAME),
634                ]
635                .into_iter()
636                .collect(),
637            );
638        }
639        Ok(publishers)
640    }
641
642    /// Subscribes to notifications from this client's chain.
643    #[instrument(level = "trace")]
644    pub fn subscribe(&self) -> Result<NotificationStream, LocalNodeError> {
645        self.subscribe_to(self.chain_id)
646    }
647
648    /// Subscribes to notifications from the specified chain.
649    #[instrument(level = "trace")]
650    pub fn subscribe_to(&self, chain_id: ChainId) -> Result<NotificationStream, LocalNodeError> {
651        Ok(Box::pin(UnboundedReceiverStream::new(
652            self.client.notifier.subscribe(vec![chain_id]),
653        )))
654    }
655
656    /// Returns the storage client used by this client's local node.
657    #[instrument(level = "trace")]
658    pub fn storage_client(&self) -> &Env::Storage {
659        self.client.storage_client()
660    }
661
662    /// Obtains the basic `ChainInfo` data for the local chain.
663    #[instrument(level = "trace")]
664    pub async fn chain_info(&self) -> Result<Box<ChainInfo>, LocalNodeError> {
665        let query = ChainInfoQuery::new(self.chain_id);
666        let response = self
667            .client
668            .local_node
669            .handle_chain_info_query(query)
670            .await?;
671        Ok(response.info)
672    }
673
674    /// Obtains the basic `ChainInfo` data for the local chain, with chain manager values.
675    #[instrument(level = "trace")]
676    pub async fn chain_info_with_manager_values(&self) -> Result<Box<ChainInfo>, LocalNodeError> {
677        let query = ChainInfoQuery::new(self.chain_id).with_manager_values();
678        let response = self
679            .client
680            .local_node
681            .handle_chain_info_query(query)
682            .await?;
683        Ok(response.info)
684    }
685
686    /// Returns a fingerprint of the chain manager's observable state: height,
687    /// current round, locking block round, and timeout certificate round.
688    ///
689    /// [`Self::process_pending_block_without_prepare`] takes one snapshot just before
690    /// each network call (re-taking it after our own local proposal handling) and
691    /// compares against the post-call state. A change means a per-validator updater
692    /// absorbed something we didn't have. The local node verifies signatures on
693    /// anything it stores, so a single dishonest validator can't fake this.
694    async fn consensus_state_snapshot(&self) -> Result<ConsensusStateSnapshot, Error> {
695        let info = self.chain_info_with_manager_values().await?;
696        let lock_round = info
697            .manager
698            .requested_locking
699            .as_deref()
700            .map(LockingBlock::round);
701        let timeout_round = info.manager.timeout.as_deref().map(|cert| cert.round);
702        Ok(ConsensusStateSnapshot {
703            next_block_height: info.next_block_height,
704            current_round: info.manager.current_round,
705            lock_round,
706            timeout_round,
707        })
708    }
709
710    /// Returns the chain's description. Fetches it from the validators if necessary.
711    pub async fn get_chain_description(&self) -> Result<ChainDescription, Error> {
712        self.client.get_chain_description(self.chain_id).await
713    }
714
715    /// Returns the description of the given application. Fetches the description blob
716    /// from the validators if necessary; the application need not be registered on
717    /// this client's chain.
718    pub async fn get_application_description(
719        &self,
720        application_id: ApplicationId,
721    ) -> Result<ApplicationDescription, Error> {
722        self.client
723            .get_application_description(application_id)
724            .await
725    }
726
727    /// Obtains up to `self.options.max_pending_message_bundles` pending message bundles for the
728    /// local chain.
729    #[instrument(level = "trace")]
730    async fn pending_message_bundles(&self) -> Result<Vec<IncomingBundle>, Error> {
731        if self.options.message_policy.is_ignore() {
732            // Ignore all messages.
733            return Ok(Vec::new());
734        }
735
736        let query = ChainInfoQuery::new(self.chain_id).with_pending_message_bundles();
737        let info = self
738            .client
739            .local_node
740            .handle_chain_info_query(query)
741            .await?
742            .info;
743        if self.preferred_owner.is_some_and(|owner| {
744            info.manager
745                .ownership
746                .is_super_owner_no_regular_owners(&owner)
747        }) {
748            // There are only super owners; they are expected to sync manually.
749            ensure!(
750                info.next_block_height >= self.initial_next_block_height,
751                Error::WalletSynchronizationError
752            );
753        }
754
755        let skipped = self.skipped_origins.pin();
756        let mut bundles = info
757            .requested_pending_message_bundles
758            .into_iter()
759            .filter_map(|bundle| bundle.apply_policy(&self.options.message_policy))
760            .filter(|bundle| !skipped.contains(&bundle.origin))
761            .collect::<Vec<_>>();
762        let priority_origins = &self.options.priority_bundle_origins;
763        bundles.sort_by(|a, b| {
764            let a_priority = priority_origins.contains(&a.origin);
765            let b_priority = priority_origins.contains(&b.origin);
766            b_priority
767                .cmp(&a_priority)
768                .then(a.bundle.timestamp.cmp(&b.bundle.timestamp))
769        });
770        bundles.truncate(self.options.max_pending_message_bundles);
771        Ok(bundles)
772    }
773
774    #[instrument(level = "trace")]
775    async fn collect_stream_updates(&self) -> Result<Vec<Operation>, Error> {
776        let subscription_map = self
777            .client
778            .local_node
779            .get_event_subscriptions(self.chain_id)
780            .await?;
781        let futures = subscription_map
782            .into_iter()
783            .filter(|((chain_id, _), _)| {
784                self.options
785                    .message_policy
786                    .restrict_chain_ids_to
787                    .as_ref()
788                    .is_none_or(|chain_set| chain_set.contains(chain_id))
789            })
790            .filter(|((_, stream_id), _)| {
791                self.options
792                    .message_policy
793                    .process_events_from_application_ids
794                    .as_ref()
795                    .is_none_or(|app_set| app_set.contains(&stream_id.application_id))
796            })
797            .map(|((chain_id, stream_id), subscriptions)| {
798                let client = self.client.clone();
799                async move {
800                    let counts = client
801                        .local_node
802                        .get_stream_indices(chain_id, stream_id.clone())
803                        .await?;
804                    let (first_index, next_index) = (counts.first_index, counts.next_index);
805                    if next_index <= subscriptions.min_next_index {
806                        return Ok::<_, Error>(Vec::new());
807                    }
808                    Ok(subscriptions
809                        .applications
810                        .into_iter()
811                        .filter(|(_, app_index)| *app_index < next_index)
812                        .map(|(application_id, _)| {
813                            SystemOperation::UpdateStream {
814                                application_id,
815                                chain_id,
816                                stream_id: stream_id.clone(),
817                                first_index,
818                                next_index,
819                            }
820                            .into()
821                        })
822                        .collect::<Vec<Operation>>())
823                }
824            });
825        Ok(futures::stream::iter(futures)
826            .buffer_unordered(self.options.max_joined_tasks)
827            .try_collect::<Vec<_>>()
828            .await?
829            .into_iter()
830            .flatten()
831            .collect::<Vec<_>>())
832    }
833
834    /// Obtains the committee for the current epoch of the local chain.
835    #[instrument(level = "trace")]
836    pub async fn local_committee(&self) -> Result<Arc<Committee>, Error> {
837        let info = match self.client.local_node.chain_info(self.chain_id).await {
838            Ok(info) => info,
839            Err(LocalNodeError::BlobsNotFound(_)) => {
840                self.synchronize_chain_state(self.chain_id).await?;
841                self.client.local_node.chain_info(self.chain_id).await?
842            }
843            Err(err) => return Err(err.into()),
844        };
845        let hash = info
846            .committee_hash
847            .ok_or(LocalNodeError::InactiveChain(self.chain_id))?;
848        Ok(self
849            .storage_client()
850            .get_or_load_committee_by_hash(hash)
851            .await
852            .map_err(LocalNodeError::from)?)
853    }
854
855    /// Obtains the committee for the latest epoch on the admin chain.
856    #[instrument(level = "trace")]
857    pub async fn admin_committee(&self) -> Result<(Epoch, Arc<Committee>), LocalNodeError> {
858        self.client.admin_committee().await
859    }
860
861    /// Obtains the identity of the current owner of the chain.
862    ///
863    /// Returns an error if we don't have the private key for the identity.
864    #[instrument(level = "trace")]
865    pub async fn identity(&self) -> Result<AccountOwner, Error> {
866        let Some(preferred_owner) = self.preferred_owner else {
867            return Err(Error::NoAccountKeyConfigured(self.chain_id));
868        };
869        let manager = self.chain_info().await?.manager;
870        ensure!(
871            manager.ownership.is_active(),
872            LocalNodeError::InactiveChain(self.chain_id)
873        );
874        let fallback_owners = if manager.ownership.has_fallback() {
875            self.local_committee()
876                .await?
877                .account_keys_and_weights()
878                .map(|(key, _)| AccountOwner::from(key))
879                .collect()
880        } else {
881            BTreeSet::new()
882        };
883
884        let is_owner = manager
885            .ownership
886            .can_propose_in_multi_leader_round(&preferred_owner)
887            || fallback_owners.contains(&preferred_owner);
888
889        if !is_owner {
890            warn!(
891                chain_id = %self.chain_id,
892                ownership = ?manager.ownership,
893                ?fallback_owners,
894                ?preferred_owner,
895                "The preferred owner is not configured as an owner of this chain",
896            );
897            return Err(Error::NotAnOwner(self.chain_id));
898        }
899
900        let has_signer = self.has_key_for(&preferred_owner).await?;
901
902        if !has_signer {
903            warn!(%self.chain_id, ?preferred_owner,
904                "Chain is one of the owners but its Signer instance doesn't contain the key",
905            );
906            return Err(Error::CannotFindKeyForChain(self.chain_id));
907        }
908
909        Ok(preferred_owner)
910    }
911
912    /// Prepares the chain for a new owner by fetching the chain description and validating access.
913    ///
914    /// This is useful when assigning a chain to a client that may not have the owner key,
915    /// e.g. when a faucet creates a chain with `open_multi_leader_rounds`.
916    ///
917    /// Returns the chain info if the owner can propose on this chain (either because they are
918    /// an owner, or because `open_multi_leader_rounds` is enabled).
919    #[instrument(level = "trace")]
920    pub async fn prepare_for_owner(&self, owner: AccountOwner) -> Result<Box<ChainInfo>, Error> {
921        ensure!(
922            self.has_key_for(&owner).await?,
923            Error::CannotFindKeyForChain(self.chain_id)
924        );
925        // Ensure we have the chain description blob.
926        self.client
927            .get_chain_description_blob(self.chain_id)
928            .await?;
929
930        // Get chain info.
931        let info = self.chain_info().await?;
932
933        // Validate that the owner can propose on this chain.
934        ensure!(
935            info.manager
936                .ownership
937                .can_propose_in_multi_leader_round(&owner),
938            Error::NotAnOwner(self.chain_id)
939        );
940
941        Ok(info)
942    }
943
944    /// Prepares the chain for the next operation, i.e. makes sure we have synchronized it up to
945    /// its current height.
946    #[instrument(level = "trace")]
947    pub async fn prepare_chain(&self) -> Result<Box<ChainInfo>, Error> {
948        #[cfg(with_metrics)]
949        let _latency = super::metrics::PREPARE_CHAIN_LATENCY.measure_latency();
950
951        let mut info = self.synchronize_to_known_height().await?;
952
953        if self.preferred_owner.is_none_or(|owner| {
954            !info
955                .manager
956                .ownership
957                .is_super_owner_no_regular_owners(&owner)
958        }) {
959            // If we are not a super owner or there are regular owners, we could be missing recent
960            // certificates created by other clients. Further synchronize blocks from the network.
961            // This is a best-effort that depends on network conditions.
962            info = self.client.synchronize_chain_state(self.chain_id).await?;
963        }
964
965        if info.epoch > self.client.admin_committee().await?.0 {
966            self.client
967                .synchronize_chain_state(self.client.admin_chain_id)
968                .await?;
969        }
970
971        Ok(info)
972    }
973
974    // Verifies that our local storage contains enough history compared to the
975    // known block height. Otherwise, downloads the missing history from the
976    // network.
977    // The known height only differs if the wallet is ahead of storage.
978    async fn synchronize_to_known_height(&self) -> Result<Box<ChainInfo>, Error> {
979        let info = self
980            .client
981            .download_certificates(self.chain_id, self.initial_next_block_height)
982            .await?;
983        if info.next_block_height == self.initial_next_block_height {
984            // Check that our local node has the expected block hash.
985            ensure!(
986                self.initial_block_hash == info.block_hash,
987                Error::InternalError("Invalid chain of blocks in local node")
988            );
989        }
990        Ok(info)
991    }
992
993    /// Attempts to update all validators about the local chain.
994    #[instrument(level = "trace", skip(old_committee, latest_certificate))]
995    pub async fn update_validators(
996        &self,
997        old_committee: Option<&Committee>,
998        latest_certificate: Option<CacheArc<ConfirmedBlockCertificate>>,
999    ) -> Result<(), Error> {
1000        let update_validators_start = linera_base::time::Instant::now();
1001        // Communicate the new certificate now.
1002        if let Some(old_committee) = old_committee {
1003            self.communicate_chain_updates(old_committee, latest_certificate.clone())
1004                .await?
1005        };
1006        if let Ok(new_committee) = self.local_committee().await {
1007            if old_committee.is_none_or(|old| *new_committee != *old) {
1008                // If the configuration just changed, communicate to the new committee as well.
1009                // (This is actually more important that updating the previous committee.)
1010                self.communicate_chain_updates(&new_committee, latest_certificate)
1011                    .await?;
1012            }
1013        }
1014        self.send_timing(update_validators_start, TimingType::UpdateValidators);
1015        Ok(())
1016    }
1017
1018    /// Broadcasts certified blocks to validators.
1019    #[instrument(level = "trace", skip(committee))]
1020    pub async fn communicate_chain_updates(
1021        &self,
1022        committee: &Committee,
1023        latest_certificate: Option<CacheArc<ConfirmedBlockCertificate>>,
1024    ) -> Result<(), Error> {
1025        let delivery = self.options.cross_chain_message_delivery;
1026        let height = self.chain_info().await?.next_block_height;
1027        self.client
1028            .communicate_chain_updates(
1029                committee,
1030                self.chain_id,
1031                height,
1032                delivery,
1033                latest_certificate,
1034            )
1035            .await
1036    }
1037
1038    /// Synchronizes all chains that any application on this chain subscribes to.
1039    /// We always consider the admin chain a relevant publishing chain, for new epochs.
1040    /// For event publisher chains, only event-bearing blocks are downloaded (partial sync).
1041    async fn synchronize_publisher_chains(&self) -> Result<(), Error> {
1042        let subscriptions = self
1043            .client
1044            .local_node
1045            .get_event_subscriptions(self.chain_id)
1046            .await?;
1047        // Group subscribed streams by publisher chain.
1048        let mut streams_by_chain = BTreeMap::<ChainId, BTreeSet<StreamId>>::new();
1049        for ((chain_id, stream_id), _) in &subscriptions {
1050            if *chain_id != self.chain_id {
1051                streams_by_chain
1052                    .entry(*chain_id)
1053                    .or_default()
1054                    .insert(stream_id.clone());
1055            }
1056        }
1057        // Always fully sync the admin chain for epoch changes.
1058        let admin_chain_id = self.client.admin_chain_id;
1059        if admin_chain_id != self.chain_id {
1060            self.client.synchronize_chain_state(admin_chain_id).await?;
1061        }
1062        // For event publisher chains, do a partial sync using previous_event_blocks.
1063        let (_, committee) = self.admin_committee().await?;
1064        let nodes = self.client.make_nodes(&committee)?;
1065        let tasks = streams_by_chain
1066            .into_iter()
1067            .filter(|(chain_id, _)| *chain_id != admin_chain_id)
1068            .map(|(chain_id, stream_ids)| {
1069                self.sync_publisher_chain_events(chain_id, stream_ids, &nodes, &committee)
1070            })
1071            .collect::<Vec<_>>();
1072        stream::iter(tasks)
1073            .buffer_unordered(self.options.max_joined_tasks)
1074            .collect::<Vec<_>>()
1075            .await
1076            .into_iter()
1077            .collect::<Result<Vec<_>, _>>()?;
1078        Ok(())
1079    }
1080
1081    /// Downloads only event-bearing blocks for the given publisher chain and streams.
1082    ///
1083    /// Uses `communicate_with_quorum` so that each validator is queried for
1084    /// `previous_event_blocks` and the claimed blocks are downloaded from that same
1085    /// validator. Waiting for a quorum guarantees that any confirmed event is discovered,
1086    /// since at least one honest validator in the quorum must have it.
1087    async fn sync_publisher_chain_events(
1088        &self,
1089        publisher_chain_id: ChainId,
1090        stream_ids: BTreeSet<StreamId>,
1091        nodes: &[RemoteNode<Env::ValidatorNode>],
1092        committee: &Committee,
1093    ) -> Result<(), Error> {
1094        let stream_ids_ref = &stream_ids;
1095        communicate_with_quorum(
1096            nodes,
1097            committee,
1098            |_: &()| (),
1099            |remote_node| async move {
1100                self.client
1101                    .sync_events_from_node(publisher_chain_id, stream_ids_ref, &remote_node)
1102                    .await
1103            },
1104            self.options.quorum_grace_period,
1105        )
1106        .await?;
1107        Ok(())
1108    }
1109
1110    /// Attempts to download new received certificates.
1111    ///
1112    /// This is a best effort: it will only find certificates that have been confirmed
1113    /// amongst sufficiently many validators of the current committee of the target
1114    /// chain.
1115    ///
1116    /// However, this should be the case whenever a sender's chain is still in use and
1117    /// is regularly upgraded to new committees.
1118    #[instrument(level = "debug", skip(self), fields(chain_id = %self.chain_id))]
1119    pub async fn find_received_certificates(&self) -> Result<(), Error> {
1120        debug!("starting find_received_certificates");
1121        let sync_start = Instant::now();
1122        #[cfg(with_metrics)]
1123        let _latency = super::metrics::FIND_RECEIVED_CERTIFICATES_LATENCY.measure_latency();
1124        // Use network information from the local chain.
1125        let chain_id = self.chain_id;
1126        let (_, committee) = self.admin_committee().await?;
1127        let nodes = self.client.make_nodes(&committee)?;
1128
1129        let trackers = self
1130            .client
1131            .local_node
1132            .get_received_certificate_trackers(chain_id)
1133            .await?;
1134
1135        trace!("find_received_certificates: read trackers");
1136
1137        let received_log_batches = Arc::new(std::sync::Mutex::new(Vec::new()));
1138        // Proceed to downloading received logs.
1139        let result = communicate_with_quorum(
1140            &nodes,
1141            &committee,
1142            |_| (),
1143            |remote_node| {
1144                let client = &self.client;
1145                let tracker = trackers.get(&remote_node.public_key).copied().unwrap_or(0);
1146                let received_log_batches = Arc::clone(&received_log_batches);
1147                Box::pin(async move {
1148                    let batch = client
1149                        .get_received_log_from_validator(chain_id, &remote_node, tracker)
1150                        .await?;
1151                    let mut batches = received_log_batches.lock().unwrap();
1152                    batches.push((remote_node.public_key, batch));
1153                    Ok(())
1154                })
1155            },
1156            self.options.quorum_grace_period,
1157        )
1158        .await;
1159
1160        if let Err(error) = result {
1161            error!(
1162                %error,
1163                "Failed to synchronize received_logs from at least a quorum of validators",
1164            );
1165        }
1166
1167        let received_logs: Vec<_> = {
1168            let mut received_log_batches = received_log_batches.lock().unwrap();
1169            std::mem::take(received_log_batches.as_mut())
1170        };
1171
1172        let received_logs_total = received_logs.iter().map(|x| x.1.len()).sum::<usize>();
1173        // How far behind each validator's tracker was. These differ by orders of magnitude in
1174        // practice — one validator can be millions of entries behind while the rest are a few
1175        // thousand — and it is the per-validator number that says which walk is expensive.
1176        let entries_per_validator = received_logs
1177            .iter()
1178            .map(|(public_key, batch)| (*public_key, batch.len()))
1179            .collect::<Vec<_>>();
1180        #[cfg(with_metrics)]
1181        super::metrics::FIND_RECEIVED_CERTIFICATES_LOG_ENTRIES
1182            .with_label_values(&[])
1183            .observe(received_logs_total as f64);
1184
1185        debug!(
1186            received_logs_len = %received_logs.len(),
1187            %received_logs_total,
1188            "collected received logs"
1189        );
1190
1191        let (received_logs, mut validator_trackers) = {
1192            (
1193                ReceivedLogs::from_received_result(received_logs.clone()),
1194                ValidatorTrackers::new(received_logs, &trackers),
1195            )
1196        };
1197
1198        #[cfg(with_metrics)]
1199        {
1200            super::metrics::FIND_RECEIVED_CERTIFICATES_SENDER_CHAINS
1201                .with_label_values(&[])
1202                .observe(received_logs.num_chains() as f64);
1203            super::metrics::SENDER_CERTIFICATES_DISCOVERED_TOTAL
1204                .inc_by(received_logs.num_certs() as u64);
1205        }
1206
1207        debug!(
1208            num_chains = %received_logs.num_chains(),
1209            num_certs = %received_logs.num_certs(),
1210            "find_received_certificates: total number of chains and certificates to sync",
1211        );
1212
1213        let num_certs = received_logs.num_certs();
1214        let is_large_sync = num_certs >= LARGE_RECEIVED_CERTIFICATE_SYNC_THRESHOLD;
1215        if is_large_sync {
1216            // Resolved here rather than above so the common, quiet path allocates nothing.
1217            let entries_per_validator = entries_per_validator
1218                .iter()
1219                .map(|(public_key, entries)| {
1220                    let address = nodes
1221                        .iter()
1222                        .find(|node| node.public_key == *public_key)
1223                        .map_or_else(|| public_key.to_string(), |node| node.address());
1224                    (address, *entries)
1225                })
1226                .collect::<Vec<_>>();
1227            info!(
1228                %chain_id,
1229                num_chains = %received_logs.num_chains(),
1230                %num_certs,
1231                num_log_entries = %received_logs_total,
1232                ?entries_per_validator,
1233                "starting a large sync of received certificates",
1234            );
1235        }
1236        let max_blocks_per_chain =
1237            self.options.sender_certificate_download_batch_size / self.options.max_joined_tasks * 2;
1238        let mut num_synced_certs = 0;
1239        let mut next_progress_message = RECEIVED_CERTIFICATE_SYNC_PROGRESS_INTERVAL;
1240        for received_log in received_logs.into_batches(
1241            self.options.sender_certificate_download_batch_size,
1242            max_blocks_per_chain,
1243        ) {
1244            num_synced_certs += received_log.num_certs();
1245            validator_trackers = self
1246                .receive_sender_certificates(received_log, validator_trackers, &nodes)
1247                .await?;
1248
1249            self.update_received_certificate_trackers(&validator_trackers)
1250                .await;
1251
1252            if is_large_sync && num_synced_certs >= next_progress_message {
1253                info!(
1254                    %chain_id,
1255                    %num_synced_certs,
1256                    %num_certs,
1257                    "received-certificate sync in progress",
1258                );
1259                next_progress_message =
1260                    num_synced_certs + RECEIVED_CERTIFICATE_SYNC_PROGRESS_INTERVAL;
1261            }
1262        }
1263
1264        if is_large_sync {
1265            info!(
1266                %chain_id,
1267                %num_certs,
1268                elapsed_secs = %sync_start.elapsed().as_secs(),
1269                "finished a large sync of received certificates",
1270            );
1271        }
1272
1273        trace!("find_received_certificates finished");
1274
1275        Ok(())
1276    }
1277
1278    async fn update_received_certificate_trackers(&self, trackers: &ValidatorTrackers) {
1279        let updated_trackers = trackers.to_map();
1280        trace!(?updated_trackers, "updated tracker values");
1281
1282        // Update the trackers.
1283        if let Err(error) = self
1284            .client
1285            .local_node
1286            .update_received_certificate_trackers(self.chain_id, updated_trackers)
1287            .await
1288        {
1289            error!(
1290                chain_id = %self.chain_id,
1291                %error,
1292                "Failed to update the certificate trackers",
1293            );
1294        }
1295    }
1296
1297    /// Downloads and processes or preprocesses the certificates for blocks sending messages to
1298    /// this chain that we are still missing.
1299    async fn receive_sender_certificates(
1300        &self,
1301        mut received_logs: ReceivedLogs,
1302        mut validator_trackers: ValidatorTrackers,
1303        nodes: &[RemoteNode<Env::ValidatorNode>],
1304    ) -> Result<ValidatorTrackers, Error> {
1305        debug!(
1306            num_chains = %received_logs.num_chains(),
1307            num_certs = %received_logs.num_certs(),
1308            "receive_sender_certificates: number of chains and certificates to sync",
1309        );
1310
1311        // Obtain the next block height we need in the local node, for each chain.
1312        let local_next_heights = self
1313            .client
1314            .local_node
1315            .next_outbox_heights(received_logs.chains(), self.chain_id)
1316            .await?;
1317
1318        validator_trackers.filter_out_already_known(&mut received_logs, &local_next_heights);
1319
1320        #[cfg(with_metrics)]
1321        super::metrics::SENDER_CERTIFICATES_MISSING_TOTAL.inc_by(received_logs.num_certs() as u64);
1322
1323        debug!(
1324            remaining_total_certificates = %received_logs.num_certs(),
1325            "receive_sender_certificates: computed remote_heights"
1326        );
1327
1328        let mut other_sender_chains = Vec::new();
1329        let (sender, mut receiver) = mpsc::unbounded_channel::<ChainAndHeight>();
1330
1331        let cert_futures = received_logs.heights_per_chain().into_iter().filter_map({
1332            let received_logs = &received_logs;
1333            let other_sender_chains = &mut other_sender_chains;
1334
1335            move |(sender_chain_id, remote_heights)| {
1336                if remote_heights.is_empty() {
1337                    // Our highest, locally executed block is higher than any block height
1338                    // from the current batch. Skip this batch, but remember to wait for
1339                    // the messages to be delivered to the inboxes.
1340                    other_sender_chains.push(sender_chain_id);
1341                    return None;
1342                };
1343                let remote_heights = remote_heights.into_iter().collect::<Vec<_>>();
1344                let sender = sender.clone();
1345                let client = self.client.clone();
1346                let nodes = nodes.to_vec();
1347                Some(async move {
1348                    client
1349                        .download_and_process_sender_chain(
1350                            sender_chain_id,
1351                            &nodes,
1352                            received_logs,
1353                            remote_heights,
1354                            sender,
1355                        )
1356                        .await
1357                })
1358            }
1359        });
1360
1361        future::join(
1362            stream::iter(cert_futures)
1363                .buffer_unordered(self.options.max_joined_tasks)
1364                .collect::<()>(),
1365            async {
1366                while let Some(chain_and_height) = receiver.recv().await {
1367                    validator_trackers.downloaded_cert(chain_and_height);
1368                }
1369            },
1370        )
1371        .await;
1372
1373        debug!(
1374            num_other_chains = %other_sender_chains.len(),
1375            "receive_sender_certificates: processing certificates finished"
1376        );
1377
1378        // Certificates for these chains were omitted from `certificates` because they were
1379        // already processed locally. If they were processed in a concurrent task, it is not
1380        // guaranteed that their cross-chain messages were already handled.
1381        self.retry_pending_cross_chain_requests_from_sender_chains(nodes, other_sender_chains)
1382            .await;
1383
1384        debug!("receive_sender_certificates: finished processing other_sender_chains");
1385
1386        Ok(validator_trackers)
1387    }
1388
1389    /// Retries cross chain requests on the chains which may have been processed on
1390    /// another task without the messages being correctly handled. Fetches missing blobs from
1391    /// the given nodes if necessary.
1392    async fn retry_pending_cross_chain_requests_from_sender_chains(
1393        &self,
1394        nodes: &[RemoteNode<Env::ValidatorNode>],
1395        other_sender_chains: Vec<ChainId>,
1396    ) {
1397        let stream = other_sender_chains
1398            .into_iter()
1399            .map(|chain_id| async move {
1400                if let Err(error) = match self
1401                    .client
1402                    .retry_pending_cross_chain_requests(chain_id)
1403                    .await
1404                {
1405                    Ok(()) => Ok(()),
1406                    Err(LocalNodeError::BlobsNotFound(blob_ids)) => {
1407                        if let Err(error) = self
1408                            .client
1409                            .update_local_node_with_blobs_from(blob_ids.clone(), nodes)
1410                            .await
1411                        {
1412                            error!(
1413                                ?blob_ids,
1414                                %error,
1415                                "Error while attempting to download blobs during retrying outgoing \
1416                                messages"
1417                            );
1418                        }
1419                        self.client
1420                            .retry_pending_cross_chain_requests(chain_id)
1421                            .await
1422                    }
1423                    err => err,
1424                } {
1425                    error!(
1426                        %chain_id,
1427                        %error,
1428                        "Failed to retry outgoing messages from chain"
1429                    );
1430                }
1431            })
1432            .collect::<FuturesUnordered<_>>();
1433        stream.for_each(future::ready).await;
1434    }
1435
1436    /// Sends money.
1437    #[instrument(level = "trace")]
1438    pub async fn transfer(
1439        &self,
1440        owner: AccountOwner,
1441        amount: Amount,
1442        recipient: Account,
1443    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
1444        // TODO(#467): check the balance of `owner` before signing any block proposal.
1445        self.execute_operation(SystemOperation::Transfer {
1446            owner,
1447            recipient,
1448            amount,
1449        })
1450        .await
1451    }
1452
1453    /// Verify if a data blob is readable from storage.
1454    // TODO(#2490): Consider removing or renaming this.
1455    #[instrument(level = "trace")]
1456    pub async fn read_data_blob(
1457        &self,
1458        hash: CryptoHash,
1459    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
1460        let blob_id = BlobId {
1461            hash,
1462            blob_type: BlobType::Data,
1463        };
1464        self.execute_operation(SystemOperation::VerifyBlob { blob_id })
1465            .await
1466    }
1467
1468    /// Claims money in a remote chain.
1469    #[instrument(level = "trace")]
1470    pub async fn claim(
1471        &self,
1472        owner: AccountOwner,
1473        target_id: ChainId,
1474        recipient: Account,
1475        amount: Amount,
1476    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
1477        self.execute_operation(SystemOperation::Claim {
1478            owner,
1479            target_id,
1480            recipient,
1481            amount,
1482        })
1483        .await
1484    }
1485
1486    /// Requests a leader timeout vote from all validators. If a quorum signs it, creates a
1487    /// certificate and sends it to all validators, to make them enter the next round.
1488    #[instrument(level = "trace")]
1489    pub async fn request_leader_timeout(&self) -> Result<TimeoutCertificate, Error> {
1490        let chain_id = self.chain_id;
1491        let info = self.chain_info().await?;
1492        let committee = self.local_committee().await?;
1493        let height = info.next_block_height;
1494        let round = info.manager.current_round;
1495        let action = CommunicateAction::RequestTimeout {
1496            height,
1497            round,
1498            chain_id,
1499        };
1500        let value = Timeout::new(chain_id, height, info.epoch);
1501        let certificate = Box::new(
1502            self.client
1503                .communicate_chain_action(&committee, action, value)
1504                .await?,
1505        );
1506        self.client
1507            .handle_certificate::<Timeout>(*certificate.clone())
1508            .await?;
1509        // The block height didn't increase, but this will communicate the timeout as well.
1510        self.client
1511            .communicate_chain_updates(
1512                &committee,
1513                chain_id,
1514                height,
1515                CrossChainMessageDelivery::NonBlocking,
1516                None,
1517            )
1518            .await?;
1519        Ok(*certificate)
1520    }
1521
1522    /// Downloads and processes any certificates we are missing for the given chain.
1523    #[instrument(level = "trace", skip_all)]
1524    pub async fn synchronize_chain_state(
1525        &self,
1526        chain_id: ChainId,
1527    ) -> Result<Box<ChainInfo>, Error> {
1528        self.client.synchronize_chain_state(chain_id).await
1529    }
1530
1531    /// Downloads and processes any certificates we are missing for this chain, from the given
1532    /// committee.
1533    #[instrument(level = "trace", skip_all)]
1534    pub async fn synchronize_chain_state_from_committee(
1535        &self,
1536        committee: Arc<Committee>,
1537    ) -> Result<Box<ChainInfo>, Error> {
1538        self.client
1539            .synchronize_chain_from_committee(self.chain_id, committee)
1540            .await
1541    }
1542
1543    /// Executes a list of operations.
1544    #[instrument(level = "trace", skip(operations, blobs))]
1545    pub async fn execute_operations(
1546        &self,
1547        operations: Vec<Operation>,
1548        blobs: Vec<Blob>,
1549    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
1550        let timing_start = linera_base::time::Instant::now();
1551
1552        let result = loop {
1553            let execute_block_start = linera_base::time::Instant::now();
1554            // TODO(#2066): Remove boxing once the call-stack is shallower
1555            match Box::pin(self.execute_block(operations.clone(), blobs.clone())).await {
1556                Ok(ClientOutcome::Committed(certificate)) => {
1557                    self.send_timing(execute_block_start, TimingType::ExecuteBlock);
1558                    break Ok(ClientOutcome::Committed(certificate));
1559                }
1560                Ok(ClientOutcome::WaitForTimeout(timeout)) => {
1561                    break Ok(ClientOutcome::WaitForTimeout(timeout));
1562                }
1563                Ok(ClientOutcome::Conflict(certificate)) => {
1564                    info!(
1565                        height = %certificate.block().header.height,
1566                        "Another block was committed."
1567                    );
1568                    break Ok(ClientOutcome::Conflict(certificate));
1569                }
1570                Err(Error::CommunicationError(CommunicationError::Trusted(
1571                    NodeError::UnexpectedBlockHeight {
1572                        expected_block_height,
1573                        found_block_height,
1574                    },
1575                ))) if expected_block_height > found_block_height => {
1576                    tracing::info!(
1577                        chain_id = %self.chain_id,
1578                        "Local state is outdated; synchronizing chain"
1579                    );
1580                    self.synchronize_chain_state(self.chain_id).await?;
1581                }
1582                Err(err) => return Err(err),
1583            };
1584        };
1585
1586        self.send_timing(timing_start, TimingType::ExecuteOperations);
1587
1588        result
1589    }
1590
1591    /// Executes an operation.
1592    pub async fn execute_operation(
1593        &self,
1594        operation: impl Into<Operation>,
1595    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
1596        self.execute_operations(vec![operation.into()], vec![])
1597            .await
1598    }
1599
1600    /// Executes a new block.
1601    ///
1602    /// This must be preceded by a call to `prepare_chain()`.
1603    #[instrument(level = "trace", skip(operations, blobs))]
1604    async fn execute_block(
1605        &self,
1606        operations: Vec<Operation>,
1607        blobs: Vec<Blob>,
1608    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
1609        #[cfg(with_metrics)]
1610        let _latency = super::metrics::EXECUTE_BLOCK_LATENCY.measure_latency();
1611
1612        let result = self.try_execute_block(operations, blobs).await;
1613        if let Err(error) = &result {
1614            let error_type = error.error_type();
1615            #[cfg(with_metrics)]
1616            super::metrics::BLOCK_STAGING_FAILURES_TOTAL
1617                .with_label_values(&[error_type.as_str()])
1618                .inc();
1619            info!(chain_id = %self.chain_id, %error_type, "Block staging failed");
1620        }
1621        result
1622    }
1623
1624    async fn try_execute_block(
1625        &self,
1626        operations: Vec<Operation>,
1627        blobs: Vec<Blob>,
1628    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
1629        let mutex = self.proposal_mutex();
1630        let lock_start = linera_base::time::Instant::now();
1631        let mut proposal_guard = mutex.lock_owned().await;
1632        tracing::debug!(
1633            chain_id = %self.chain_id,
1634            lock_wait_ms = lock_start.elapsed().as_millis(),
1635            "acquired proposal_mutex in execute_block"
1636        );
1637        // TODO(#5092): We shouldn't need to call this explicitly.
1638        // Process any leftover pending proposal from a previous interrupted call.
1639        // Even if there is no pending proposal, this still calls
1640        // `request_leader_timeout_if_needed` which ensures the local chain state
1641        // is synchronized with the current consensus round.
1642        match self
1643            .process_pending_block_without_prepare(&mut proposal_guard)
1644            .await?
1645        {
1646            ClientOutcome::Committed(Some(certificate)) => {
1647                return Ok(self.classify_committed(certificate, &operations));
1648            }
1649            ClientOutcome::WaitForTimeout(timeout) => {
1650                return Ok(ClientOutcome::WaitForTimeout(timeout))
1651            }
1652            ClientOutcome::Conflict(certificate) => {
1653                return Ok(ClientOutcome::Conflict(certificate))
1654            }
1655            ClientOutcome::Committed(None) => {}
1656        }
1657
1658        loop {
1659            // Collect pending messages and epoch changes after acquiring the lock to avoid
1660            // race conditions where messages valid for one block height are proposed at a
1661            // different height. This is recomputed on every retry because the set of
1662            // receivable messages and epoch changes can differ at a new height.
1663            let transactions = self
1664                .prepend_epochs_messages_and_events(operations.clone())
1665                .await?;
1666
1667            if transactions.is_empty() {
1668                return Err(Error::LocalNodeError(LocalNodeError::WorkerError(
1669                    WorkerError::ChainError(Box::new(ChainError::EmptyBlock)),
1670                )));
1671            }
1672
1673            self.new_pending_block(transactions, blobs.clone(), &mut proposal_guard)
1674                .await?;
1675
1676            match self
1677                .process_pending_block_without_prepare(&mut proposal_guard)
1678                .await?
1679            {
1680                ClientOutcome::Committed(Some(certificate)) => {
1681                    return Ok(self.classify_committed(certificate, &operations));
1682                }
1683                // `process_pending_block_without_prepare` cleared the pending proposal
1684                // without committing ours, so our operations were not applied. This happens
1685                // in two ways:
1686                //   * the chain advanced past the height we staged at while we were
1687                //     proposing (e.g. a notification or background sync committed another
1688                //     block at our height) — the common #5664 case; or
1689                //   * the staged proposal's authenticated signer's key is no longer held
1690                //     (the preferred owner changed since we staged), so the stale proposal
1691                //     was discarded.
1692                // Either way, re-stage and retry: `new_pending_block` recomputes the block
1693                // at the current height and signs as the current identity, so the first
1694                // case advances to the new height (genuine external progress) and the
1695                // second adopts a signer we hold, so the retry converges. See #5664.
1696                ClientOutcome::Committed(None) => {
1697                    tracing::debug!(
1698                        chain_id = %self.chain_id,
1699                        "pending proposal cleared without committing ours; re-staging and retrying"
1700                    );
1701                    continue;
1702                }
1703                ClientOutcome::WaitForTimeout(timeout) => {
1704                    return Ok(ClientOutcome::WaitForTimeout(timeout));
1705                }
1706                ClientOutcome::Conflict(certificate) => {
1707                    return Ok(ClientOutcome::Conflict(certificate));
1708                }
1709            }
1710        }
1711    }
1712
1713    /// Returns `Committed` if the committed block reflects our request — same
1714    /// authenticated owner, and our `operations`. All other transactions must be ones that
1715    /// are added automatically, to process messages, streams or new epochs.
1716    fn classify_committed(
1717        &self,
1718        certificate: ConfirmedBlockCertificate,
1719        operations: &[Operation],
1720    ) -> ClientOutcome<ConfirmedBlockCertificate> {
1721        let block = certificate.block();
1722        if self.preferred_owner.is_none()
1723            || block.header.authenticated_owner != self.preferred_owner
1724        {
1725            return ClientOutcome::Conflict(Box::new(certificate));
1726        }
1727        let mut operations_iter = operations.iter().peekable();
1728        for tx in &block.body.transactions {
1729            let is_expected = match tx {
1730                Transaction::ReceiveMessages(_) => true,
1731                Transaction::ExecuteOperation(op) if Some(&op) == operations_iter.peek() => {
1732                    operations_iter.next();
1733                    true
1734                }
1735                Transaction::ExecuteOperation(Operation::System(op)) => matches!(
1736                    **op,
1737                    SystemOperation::ProcessNewEpoch(_) | SystemOperation::UpdateStream { .. }
1738                ),
1739                Transaction::ExecuteOperation(Operation::User { .. }) => false,
1740            };
1741            if !is_expected {
1742                return ClientOutcome::Conflict(Box::new(certificate));
1743            }
1744        }
1745        if operations_iter.next().is_some() {
1746            ClientOutcome::Conflict(Box::new(certificate))
1747        } else {
1748            ClientOutcome::Committed(certificate)
1749        }
1750    }
1751
1752    /// Creates a vector of transactions which, in addition to the provided operations,
1753    /// also contains the next pending epoch change, receiving message bundles and event
1754    /// stream updates (if there are any to be processed).
1755    /// This should be called when executing a block, in order to make sure that any pending
1756    /// messages or events are included in it.
1757    #[instrument(level = "trace", skip(operations))]
1758    async fn prepend_epochs_messages_and_events(
1759        &self,
1760        operations: Vec<Operation>,
1761    ) -> Result<Vec<Transaction>, Error> {
1762        let incoming_bundles = self.pending_message_bundles().await?;
1763        let stream_updates = self.collect_stream_updates().await?;
1764        Ok(self
1765            .next_epoch_change()
1766            .await?
1767            .into_iter()
1768            .map(Transaction::ExecuteOperation)
1769            .chain(
1770                incoming_bundles
1771                    .into_iter()
1772                    .map(Transaction::ReceiveMessages),
1773            )
1774            .chain(
1775                stream_updates
1776                    .into_iter()
1777                    .map(Transaction::ExecuteOperation),
1778            )
1779            .chain(operations.into_iter().map(Transaction::ExecuteOperation))
1780            .collect::<Vec<_>>())
1781    }
1782
1783    /// Creates a new pending block and stores it in `proposal_guard`.
1784    ///
1785    /// The caller must hold the proposal mutex. The pending proposal is written directly
1786    /// into the guard so that it is always synchronized with the mutex.
1787    #[instrument(level = "trace", skip(transactions, blobs, proposal_guard))]
1788    async fn new_pending_block(
1789        &self,
1790        transactions: Vec<Transaction>,
1791        blobs: Vec<Blob>,
1792        proposal_guard: &mut Option<PendingProposal>,
1793    ) -> Result<Block, Error> {
1794        let identity = self.identity().await?;
1795
1796        ensure!(
1797            proposal_guard.is_none(),
1798            Error::BlockProposalError(
1799                "Client state already has a pending block; \
1800                use the `linera retry-pending-block` command to commit that first"
1801            )
1802        );
1803        let info = self.chain_info().await?;
1804        let timestamp = self.next_timestamp(&transactions, info.timestamp);
1805        let proposed_block = ProposedBlock {
1806            epoch: info.epoch,
1807            chain_id: self.chain_id,
1808            transactions,
1809            previous_block_hash: info.block_hash,
1810            height: info.next_block_height,
1811            authenticated_owner: Some(identity),
1812            timestamp,
1813        };
1814
1815        let round = self.round_for_oracle(&info, &identity).await?;
1816        // Make sure every incoming message succeeds and otherwise remove them.
1817        // Also, compute the final certified hash while we're at it.
1818        let (block, _, never_reject_origins) = self
1819            .client
1820            .stage_block_execution(
1821                proposed_block,
1822                round,
1823                blobs.clone(),
1824                self.options.bundle_execution_policy(),
1825            )
1826            .await?;
1827        // Record origins whose bundles were discarded due to the never-reject policy so
1828        // that `process_inbox` stops retrying them until the client is restarted.
1829        if !never_reject_origins.is_empty() {
1830            let skipped = self.skipped_origins.pin();
1831            for origin in never_reject_origins {
1832                skipped.insert(origin);
1833            }
1834        }
1835        let (proposed_block, auto_retry_outcome) = block.clone().into_proposal();
1836        *proposal_guard = Some(PendingProposal {
1837            block: proposed_block,
1838            blobs,
1839            auto_retry_outcome: Some(auto_retry_outcome),
1840            round: None,
1841        });
1842        Ok(block)
1843    }
1844
1845    /// Returns a suitable timestamp for the next block.
1846    ///
1847    /// This will usually be the current time according to the local clock, but may be slightly
1848    /// ahead to make sure it's not earlier than the incoming messages or the previous block.
1849    #[instrument(level = "trace", skip(transactions))]
1850    fn next_timestamp(&self, transactions: &[Transaction], block_time: Timestamp) -> Timestamp {
1851        let local_time = self.storage_client().clock().current_time();
1852        transactions
1853            .iter()
1854            .filter_map(Transaction::incoming_bundle)
1855            .map(|msg| msg.bundle.timestamp)
1856            .max()
1857            .map_or(local_time, |timestamp| timestamp.max(local_time))
1858            .max(block_time)
1859    }
1860
1861    /// Queries an application.
1862    #[instrument(level = "trace", skip(query))]
1863    pub async fn query_application(
1864        &self,
1865        query: Query,
1866        block_hash: Option<CryptoHash>,
1867    ) -> Result<(QueryOutcome, BlockHeight), Error> {
1868        let mut downloaded_blobs = HashSet::<BlobId>::new();
1869        let mut events = super::EventSetDownloader::new(&self.client);
1870        loop {
1871            let result = self
1872                .client
1873                .local_node
1874                .query_application(self.chain_id, query.clone(), block_hash)
1875                .await;
1876            if let Err(LocalNodeError::BlobsNotFound(blob_ids)) = &result {
1877                let new_blobs = super::filter_new(blob_ids, &downloaded_blobs);
1878                if !new_blobs.is_empty() {
1879                    let validators = self.client.validator_nodes().await?;
1880                    self.client
1881                        .update_local_node_with_blobs_from(new_blobs.clone(), &validators)
1882                        .await?;
1883                    downloaded_blobs.extend(new_blobs);
1884                    continue;
1885                }
1886            }
1887            if let Err(LocalNodeError::EventsNotFound(event_ids)) = &result {
1888                if events.download_new(event_ids).await? {
1889                    continue;
1890                }
1891            }
1892            return Ok(result?);
1893        }
1894    }
1895
1896    /// Queries a system application.
1897    #[cfg(with_testing)]
1898    #[instrument(level = "trace", skip(query))]
1899    pub async fn query_system_application(
1900        &self,
1901        query: linera_execution::SystemQuery,
1902    ) -> Result<QueryOutcome<linera_execution::SystemResponse>, Error> {
1903        let (
1904            QueryOutcome {
1905                response,
1906                operations,
1907            },
1908            _,
1909        ) = self.query_application(Query::System(query), None).await?;
1910        match response {
1911            linera_execution::QueryResponse::System(response) => Ok(QueryOutcome {
1912                response,
1913                operations,
1914            }),
1915            _ => Err(Error::InternalError("Unexpected response for system query")),
1916        }
1917    }
1918
1919    /// Queries a user application.
1920    #[instrument(level = "trace", skip(application_id, query))]
1921    #[cfg(with_testing)]
1922    pub async fn query_user_application<A: Abi>(
1923        &self,
1924        application_id: ApplicationId<A>,
1925        query: &A::Query,
1926    ) -> Result<QueryOutcome<A::QueryResponse>, Error> {
1927        let query = Query::user(application_id, query)?;
1928        let (
1929            QueryOutcome {
1930                response,
1931                operations,
1932            },
1933            _,
1934        ) = self.query_application(query, None).await?;
1935        match response {
1936            linera_execution::QueryResponse::User(response_bytes) => {
1937                let response = serde_json::from_slice(&response_bytes)?;
1938                Ok(QueryOutcome {
1939                    response,
1940                    operations,
1941                })
1942            }
1943            _ => Err(Error::InternalError("Unexpected response for user query")),
1944        }
1945    }
1946
1947    /// Obtains the local balance of the chain account after staging the execution of
1948    /// incoming messages in a new block.
1949    ///
1950    /// Does not attempt to synchronize with validators. The result will reflect up to
1951    /// `max_pending_message_bundles` incoming message bundles and the execution fees for a single
1952    /// block.
1953    #[instrument(level = "trace")]
1954    pub async fn query_balance(&self) -> Result<Amount, Error> {
1955        let (balance, _) = self.query_balances_with_owner(AccountOwner::CHAIN).await?;
1956        Ok(balance)
1957    }
1958
1959    /// Obtains the local balance of an account after staging the execution of incoming messages in
1960    /// a new block.
1961    ///
1962    /// Does not attempt to synchronize with validators. The result will reflect up to
1963    /// `max_pending_message_bundles` incoming message bundles and the execution fees for a single
1964    /// block.
1965    #[instrument(level = "trace", skip(owner))]
1966    pub async fn query_owner_balance(&self, owner: AccountOwner) -> Result<Amount, Error> {
1967        if owner.is_chain() {
1968            self.query_balance().await
1969        } else {
1970            Ok(self
1971                .query_balances_with_owner(owner)
1972                .await?
1973                .1
1974                .unwrap_or(Amount::ZERO))
1975        }
1976    }
1977
1978    /// Obtains the local balance of an account and optionally another user after staging the
1979    /// execution of incoming messages in a new block.
1980    ///
1981    /// Does not attempt to synchronize with validators. The result will reflect up to
1982    /// `max_pending_message_bundles` incoming message bundles and the execution fees for a single
1983    /// block.
1984    #[instrument(level = "trace", skip(owner))]
1985    pub(crate) async fn query_balances_with_owner(
1986        &self,
1987        owner: AccountOwner,
1988    ) -> Result<(Amount, Option<Amount>), Error> {
1989        let incoming_bundles = self.pending_message_bundles().await?;
1990        // Since we disallow empty blocks, and there is no incoming messages,
1991        // that could change it, we query for the balance immediately.
1992        if incoming_bundles.is_empty() {
1993            let chain_balance = self.local_balance().await?;
1994            let owner_balance = self.local_owner_balance(owner).await?;
1995            return Ok((chain_balance, Some(owner_balance)));
1996        }
1997        let info = self.chain_info().await?;
1998        let transactions = incoming_bundles
1999            .into_iter()
2000            .map(Transaction::ReceiveMessages)
2001            .collect::<Vec<_>>();
2002        let timestamp = self.next_timestamp(&transactions, info.timestamp);
2003        let block = ProposedBlock {
2004            epoch: info.epoch,
2005            chain_id: self.chain_id,
2006            transactions,
2007            previous_block_hash: info.block_hash,
2008            height: info.next_block_height,
2009            authenticated_owner: if owner == AccountOwner::CHAIN {
2010                None
2011            } else {
2012                Some(owner)
2013            },
2014            timestamp,
2015        };
2016        match self
2017            .client
2018            .stage_block_execution(
2019                block,
2020                None,
2021                Vec::new(),
2022                self.options.bundle_execution_policy(),
2023            )
2024            .await
2025        {
2026            Ok((_, response, _)) => Ok((
2027                response.info.chain_balance,
2028                response.info.requested_owner_balance,
2029            )),
2030            Err(Error::LocalNodeError(LocalNodeError::WorkerError(WorkerError::ChainError(
2031                error,
2032            )))) if matches!(
2033                &*error,
2034                ChainError::ExecutionError(
2035                    execution_error,
2036                    ChainExecutionContext::Block
2037                ) if matches!(
2038                    **execution_error,
2039                    ExecutionError::FeesExceedFunding { .. }
2040                )
2041            ) =>
2042            {
2043                // We can't even pay for the execution of one empty block. Let's return zero.
2044                Ok((Amount::ZERO, Some(Amount::ZERO)))
2045            }
2046            Err(error) => Err(error),
2047        }
2048    }
2049
2050    /// Reads the local balance of the chain account.
2051    ///
2052    /// Does not process the inbox or attempt to synchronize with validators.
2053    #[instrument(level = "trace")]
2054    pub async fn local_balance(&self) -> Result<Amount, Error> {
2055        let (balance, _) = self.local_balances_with_owner(AccountOwner::CHAIN).await?;
2056        Ok(balance)
2057    }
2058
2059    /// Reads the local balance of a user account.
2060    ///
2061    /// Does not process the inbox or attempt to synchronize with validators.
2062    #[instrument(level = "trace", skip(owner))]
2063    pub async fn local_owner_balance(&self, owner: AccountOwner) -> Result<Amount, Error> {
2064        if owner.is_chain() {
2065            self.local_balance().await
2066        } else {
2067            Ok(self
2068                .local_balances_with_owner(owner)
2069                .await?
2070                .1
2071                .unwrap_or(Amount::ZERO))
2072        }
2073    }
2074
2075    /// Reads the local balance of the chain account and optionally another user.
2076    ///
2077    /// Does not process the inbox or attempt to synchronize with validators.
2078    #[instrument(level = "trace", skip(owner))]
2079    pub(crate) async fn local_balances_with_owner(
2080        &self,
2081        owner: AccountOwner,
2082    ) -> Result<(Amount, Option<Amount>), Error> {
2083        ensure!(
2084            self.chain_info().await?.next_block_height >= self.initial_next_block_height,
2085            Error::WalletSynchronizationError
2086        );
2087        let mut query = ChainInfoQuery::new(self.chain_id);
2088        query.request_owner_balance = owner;
2089        let response = self
2090            .client
2091            .local_node
2092            .handle_chain_info_query(query)
2093            .await?;
2094        Ok((
2095            response.info.chain_balance,
2096            response.info.requested_owner_balance,
2097        ))
2098    }
2099
2100    /// Sends tokens to a chain.
2101    #[instrument(level = "trace")]
2102    pub async fn transfer_to_account(
2103        &self,
2104        from: AccountOwner,
2105        amount: Amount,
2106        account: Account,
2107    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2108        self.transfer(from, amount, account).await
2109    }
2110
2111    /// Burns tokens (transfer to a special address).
2112    #[cfg(with_testing)]
2113    #[instrument(level = "trace")]
2114    pub async fn burn(
2115        &self,
2116        owner: AccountOwner,
2117        amount: Amount,
2118    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2119        let recipient = Account::burn_address(self.chain_id);
2120        self.transfer(owner, amount, recipient).await
2121    }
2122
2123    /// Fetches the latest chain info for this chain from the validators.
2124    #[instrument(level = "trace")]
2125    pub async fn fetch_chain_info(&self) -> Result<Box<ChainInfo>, Error> {
2126        let validators = self.client.validator_nodes().await?;
2127        self.client
2128            .fetch_chain_info(self.chain_id, &validators)
2129            .await
2130    }
2131
2132    /// Attempts to synchronize chains that have sent us messages and populate our local
2133    /// inbox.
2134    ///
2135    /// To create a block that actually executes the messages in the inbox,
2136    /// `process_inbox` must be called separately.
2137    ///
2138    /// If the chain is in follow-only mode, this only downloads blocks for this chain without
2139    /// fetching manager values or sender/publisher chains.
2140    /// Synchronizes the chain state from validators, optionally stopping at a given
2141    /// block height or block timestamp.
2142    ///
2143    /// - If `next_height` is `Some`, downloads blocks up to (but not including) that height.
2144    /// - If `until_block_time` is `Some`, downloads blocks up to (but not including) the
2145    ///   first block with timestamp >= the given value.
2146    #[instrument(level = "trace")]
2147    pub async fn synchronize_up_to(
2148        &self,
2149        next_height: Option<BlockHeight>,
2150        until_block_time: Option<Timestamp>,
2151    ) -> Result<Box<ChainInfo>, Error> {
2152        let (_, committee) = self.client.admin_committee().await?;
2153        let validators = self.client.make_nodes(&committee)?;
2154        Box::pin(self.client.fetch_chain_info(self.chain_id, &validators)).await?;
2155        communicate_with_quorum(
2156            &validators,
2157            &committee,
2158            |_: &()| (),
2159            |remote_node| async move {
2160                self.client
2161                    .download_certificates_from(
2162                        &remote_node,
2163                        self.chain_id,
2164                        next_height.unwrap_or(BlockHeight::MAX),
2165                        until_block_time,
2166                    )
2167                    .await?;
2168                Ok(())
2169            },
2170            self.client.options.quorum_grace_period,
2171        )
2172        .await?;
2173        self.client
2174            .local_node
2175            .chain_info(self.chain_id)
2176            .await
2177            .map_err(Into::into)
2178    }
2179
2180    /// Synchronizes this chain's state from the validators, including received messages.
2181    pub async fn synchronize_from_validators(&self) -> Result<Box<ChainInfo>, Error> {
2182        if self.is_follow_only() {
2183            return self.client.synchronize_chain_state(self.chain_id).await;
2184        }
2185        let info = self.prepare_chain().await?;
2186        self.synchronize_publisher_chains().await?;
2187        self.find_received_certificates().await?;
2188        Ok(info)
2189    }
2190
2191    /// Processes the last pending block.
2192    #[instrument(level = "trace")]
2193    pub async fn process_pending_block(
2194        &self,
2195    ) -> Result<ClientOutcome<Option<ConfirmedBlockCertificate>>, Error> {
2196        self.prepare_chain().await?;
2197        let mutex = self.proposal_mutex();
2198        let mut proposal_guard = mutex.lock_owned().await;
2199        self.process_pending_block_without_prepare(&mut proposal_guard)
2200            .await
2201    }
2202
2203    /// Processes the last pending block. Assumes that the local chain is up to date.
2204    ///
2205    /// The caller must hold the proposal mutex via `proposal_guard`. The pending proposal
2206    /// is read from and cleared through the guard, ensuring synchronization.
2207    ///
2208    /// On error, compares the chain manager's snapshot just before the failing network
2209    /// call against the current state. If a per-validator updater absorbed new info
2210    /// (e.g. a locking block we didn't have) while we were calling, retries with the
2211    /// refreshed state. The snapshot is updated after our own local proposal handling
2212    /// so that doesn't count as "new info from a validator".
2213    ///
2214    /// Retrying terminates without an explicit bound: each retry corresponds to local
2215    /// consensus state that actually advanced (verified by the local node, which checks
2216    /// signatures and quorums), and that state is monotone.
2217    #[instrument(level = "debug", skip(self, proposal_guard), fields(chain_id = %self.chain_id))]
2218    async fn process_pending_block_without_prepare(
2219        &self,
2220        proposal_guard: &mut Option<PendingProposal>,
2221    ) -> Result<ClientOutcome<Option<ConfirmedBlockCertificate>>, Error> {
2222        // The fallback sync below is done at most once, so a genuinely stuck proposal can't loop.
2223        let mut did_fallback_sync = false;
2224        loop {
2225            // `process_pending_block_inner` records the baseline snapshot itself, after
2226            // the initial timeout request, so that absorbing a timeout certificate isn't
2227            // counted as new info. It stays `None` only if the call fails before that
2228            // point, in which case there is no baseline to retry against.
2229            let mut snapshot = None;
2230            let err = match self
2231                .process_pending_block_inner(proposal_guard, &mut snapshot)
2232                .await
2233            {
2234                Ok(outcome) => return Ok(outcome),
2235                Err(err) => err,
2236            };
2237            let Some(snapshot) = snapshot else {
2238                return Err(err);
2239            };
2240            let Ok(current) = self.consensus_state_snapshot().await else {
2241                return Err(err);
2242            };
2243            if current == snapshot {
2244                // The post-quorum pull on rejection (`Client::process_lag_reports`) only covers
2245                // validators whose rejection was actually received: `communicate_with_quorum`
2246                // breaks as soon as a quorum is impossible and drops still-in-flight requests, so
2247                // a locking block held only by the slower-to-respond validators is never even
2248                // reported. Fall back once to an explicit quorum sync — guaranteed to reach a
2249                // lock-holder — and retry if it absorbed anything; otherwise the rejection was
2250                // genuine, so propagate it.
2251                //
2252                // TODO(#6453): this fallback path has no deterministic regression test yet —
2253                // forcing a lock-holder's response to be dropped needs a clock-driven
2254                // per-validator response delay in the test harness (building on #6448); until
2255                // then it is only covered indirectly by the (now non-flaky)
2256                // `test_lazy_pull_absorbs_locking_block_on_proposal_rejection`.
2257                if did_fallback_sync {
2258                    return Err(err);
2259                }
2260                did_fallback_sync = true;
2261                if let Err(error) = self.synchronize_chain_state(self.chain_id).await {
2262                    tracing::error!(%error, "fallback sync failed after rejected proposal");
2263                    return Err(err);
2264                }
2265                let Ok(after_sync) = self.consensus_state_snapshot().await else {
2266                    return Err(err);
2267                };
2268                if after_sync == snapshot {
2269                    return Err(err);
2270                }
2271                tracing::debug!(
2272                    chain_id = %self.chain_id,
2273                    ?snapshot,
2274                    ?after_sync,
2275                    %err,
2276                    "fallback sync absorbed new consensus state after rejected proposal; retrying"
2277                );
2278                continue;
2279            }
2280            tracing::debug!(
2281                chain_id = %self.chain_id,
2282                ?snapshot,
2283                ?current,
2284                %err,
2285                "local consensus state advanced during process_pending_block; retrying"
2286            );
2287        }
2288    }
2289
2290    async fn process_pending_block_inner(
2291        &self,
2292        proposal_guard: &mut Option<PendingProposal>,
2293        snapshot: &mut Option<ConsensusStateSnapshot>,
2294    ) -> Result<ClientOutcome<Option<ConfirmedBlockCertificate>>, Error> {
2295        let process_start = linera_base::time::Instant::now();
2296        tracing::debug!("process_pending_block_without_prepare started");
2297        let info = self.request_leader_timeout_if_needed().await?;
2298        // After the timeout request (which may itself absorb a timeout cert from
2299        // validators), bake the resulting state into the snapshot. The rest of this
2300        // iteration will propose in the round the timeout produced, so a state diff
2301        // from just that absorption isn't a retry signal.
2302        *snapshot = Some(self.consensus_state_snapshot().await?);
2303
2304        // Clear stale pending proposals whose height has already been committed.
2305        if let Some(pending) = &*proposal_guard {
2306            if pending.block.height < info.next_block_height {
2307                tracing::debug!(
2308                    "Clearing pending proposal: a block was committed at height {}",
2309                    pending.block.height
2310                );
2311                *proposal_guard = None;
2312            }
2313        }
2314
2315        // If there is a validated block in the current round, finalize it.
2316        if info.manager.has_locking_block_in_current_round()
2317            && !info.manager.current_round.is_fast()
2318        {
2319            return self.finalize_locking_block(info).await;
2320        }
2321        let identity = self.identity().await?;
2322
2323        let local_node = &self.client.local_node;
2324        // Otherwise we have to re-propose the highest validated block, if there is one.
2325        let (block, blobs, owner) = if let Some(locking) = &info.manager.requested_locking {
2326            let (block, blobs) = match &**locking {
2327                LockingBlock::Regular(certificate) => {
2328                    let blob_ids = certificate.block().required_blob_ids();
2329                    let blobs = local_node
2330                        .get_locking_blobs(&blob_ids, self.chain_id)
2331                        .await?
2332                        .ok_or_else(|| Error::InternalError("Missing local locking blobs"))?;
2333                    debug!("Retrying locking block from round {}", certificate.round);
2334                    (certificate.block().clone(), blobs)
2335                }
2336                LockingBlock::Fast(proposal) => {
2337                    let proposed_block = proposal.content.block.clone();
2338                    let blob_ids = proposed_block.published_blob_ids();
2339                    let blobs = local_node
2340                        .get_locking_blobs(&blob_ids, self.chain_id)
2341                        .await?
2342                        .ok_or_else(|| Error::InternalError("Missing local locking blobs"))?;
2343                    let (block, _, _) = self
2344                        .client
2345                        .stage_block_execution(
2346                            proposed_block,
2347                            None,
2348                            blobs.clone(),
2349                            BundleExecutionPolicy::committed(),
2350                        )
2351                        .await?;
2352                    debug!("Retrying locking block from fast round.");
2353                    (block, blobs)
2354                }
2355            };
2356            (block, blobs, identity)
2357        } else if let Some(pending) = proposal_guard.as_ref() {
2358            // Otherwise we are free to propose our own pending block. Sign it as the owner
2359            // that staged it: the block's operations are authenticated by that owner, and
2360            // validators reject the proposal with `WorkerError::InvalidSigner` if we re-sign
2361            // it as someone else (which would happen if `preferred_owner` changed since the
2362            // block was staged). The signer is global to the client, so we can sign as the
2363            // original author as long as we still hold their key.
2364            let owner = match pending.block.authenticated_owner {
2365                Some(staged_owner) if staged_owner != identity => {
2366                    if !self.has_key_for(&staged_owner).await? {
2367                        // If a fast-round proposal was already submitted, we can't safely
2368                        // drop it and propose a conflicting fast block: fast rounds skip
2369                        // the validation step and rely on the super owner not forking
2370                        // itself, so a second proposal could split votes between f+1 and
2371                        // 2f and wedge the round until it times out. Surface an error so
2372                        // the caller can recover the key or wait for the timeout.
2373                        if pending.round.is_some_and(|round| round.is_fast()) {
2374                            return Err(Error::BlockProposalError(
2375                                "pending fast block was signed by an owner whose key is no \
2376                                 longer available; recover the key or wait for the round to \
2377                                 time out before retrying",
2378                            ));
2379                        }
2380                        warn!(
2381                            ?staged_owner, %identity,
2382                            "Discarding pending block: no signer key for its authenticated owner",
2383                        );
2384                        *proposal_guard = None;
2385                        return Ok(ClientOutcome::Committed(None));
2386                    }
2387                    staged_owner
2388                }
2389                _ => identity,
2390            };
2391            let proposed_block = pending.block.clone();
2392            let blobs = pending.blobs.clone();
2393            let staging_outcome = pending.auto_retry_outcome.as_ref();
2394            let round = self.round_for_oracle(&info, &owner).await?;
2395            let (block, _, _) = self
2396                .client
2397                .stage_block_execution(
2398                    proposed_block,
2399                    round,
2400                    blobs.clone(),
2401                    BundleExecutionPolicy::committed(),
2402                )
2403                .await?;
2404            // Sanity check: the committed execution should produce the same outcome
2405            // as the initial AutoRetry execution. A mismatch indicates a divergence
2406            // between the two execution paths.
2407            if let Some(staging_outcome) = staging_outcome {
2408                ensure!(
2409                    block.outcome_matches(staging_outcome),
2410                    Error::ExecutionOutcomeMismatch
2411                );
2412            }
2413            debug!("Proposing the local pending block.");
2414            (block, blobs, owner)
2415        } else {
2416            return Ok(ClientOutcome::Committed(None)); // Nothing to do.
2417        };
2418
2419        let has_oracle_responses = block.has_oracle_responses();
2420        let (proposed_block, outcome) = block.into_proposal();
2421        let round = match self
2422            .round_for_new_proposal(&info, &owner, has_oracle_responses)
2423            .await?
2424        {
2425            Either::Left(round) => round,
2426            Either::Right(timeout) => return Ok(ClientOutcome::WaitForTimeout(timeout)),
2427        };
2428        debug!("Proposing block for round {}", round);
2429        if let Some(pending) = proposal_guard.as_mut() {
2430            pending.round.get_or_insert(round);
2431        }
2432
2433        let already_handled_locally = info
2434            .manager
2435            .already_handled_proposal(round, &proposed_block);
2436        // Create the final block proposal.
2437        let proposal = if let Some(locking) = info.manager.requested_locking {
2438            Box::new(match *locking {
2439                LockingBlock::Regular(cert) => {
2440                    BlockProposal::new_retry_regular(owner, round, cert, self.signer())
2441                        .await
2442                        .map_err(Error::signer_failure)?
2443                }
2444                LockingBlock::Fast(proposal) => {
2445                    BlockProposal::new_retry_fast(owner, round, proposal, self.signer())
2446                        .await
2447                        .map_err(Error::signer_failure)?
2448                }
2449            })
2450        } else {
2451            Box::new(
2452                BlockProposal::new_initial(owner, round, proposed_block.clone(), self.signer())
2453                    .await
2454                    .map_err(Error::signer_failure)?,
2455            )
2456        };
2457        if !already_handled_locally {
2458            // Check the final block proposal. This will be cheaper after #1401.
2459            if let Err(err) = local_node.handle_block_proposal(*proposal.clone()).await {
2460                match err {
2461                    LocalNodeError::BlobsNotFound(_) => {
2462                        local_node
2463                            .handle_pending_blobs(self.chain_id, blobs)
2464                            .await?;
2465                        local_node.handle_block_proposal(*proposal.clone()).await?;
2466                    }
2467                    err => return Err(err.into()),
2468                }
2469            }
2470        }
2471        // The local handle of our own proposal can set a Fast lock, advance
2472        // `proposed`/`signed_proposal`, and thereby bump `current_round`. None of that
2473        // is "new info from a validator", so fold it into the snapshot before the
2474        // network calls below.
2475        *snapshot = Some(self.consensus_state_snapshot().await?);
2476        let committee = self.local_committee().await?;
2477        let block = Block::new(proposed_block, outcome);
2478        // Send the query to validators.
2479        let submit_block_proposal_start = linera_base::time::Instant::now();
2480        let certificate = if round.is_fast() {
2481            let hashed_value = ConfirmedBlock::new(block);
2482            self.client
2483                .submit_block_proposal(committee.clone(), proposal, hashed_value)
2484                .await?
2485        } else {
2486            let hashed_value = ValidatedBlock::new(block);
2487            let certificate = self
2488                .client
2489                .submit_block_proposal(committee.clone(), proposal, hashed_value.clone())
2490                .await?;
2491            self.client.finalize_block(&committee, certificate).await?
2492        };
2493        self.send_timing(submit_block_proposal_start, TimingType::SubmitBlockProposal);
2494        tracing::debug!(
2495            total_process_ms = process_start.elapsed().as_millis(),
2496            "process_pending_block_without_prepare completing"
2497        );
2498        debug!(round = %certificate.round, "Sending confirmed block to validators");
2499        let certificate = self.client.storage_client().cache_certificate(certificate);
2500        self.update_validators(Some(&committee), Some(certificate.clone()))
2501            .await?;
2502        // Clear the pending proposal now that the block has been committed.
2503        *proposal_guard = None;
2504        Ok(ClientOutcome::Committed(Some(CacheArc::unwrap_or_clone(
2505            certificate,
2506        ))))
2507    }
2508
2509    #[expect(
2510        clippy::cast_possible_truncation,
2511        reason = "elapsed millis fits in u64 for any realistic measurement window"
2512    )]
2513    fn send_timing(&self, start: Instant, timing_type: TimingType) {
2514        let Some(sender) = &self.timing_sender else {
2515            return;
2516        };
2517        if let Err(err) = sender.send((start.elapsed().as_millis() as u64, timing_type)) {
2518            tracing::warn!(%err, "Failed to send timing info");
2519        }
2520    }
2521
2522    /// Requests a leader timeout certificate if the current round has timed out. Returns the
2523    /// chain info for the (possibly new) current round.
2524    async fn request_leader_timeout_if_needed(&self) -> Result<Box<ChainInfo>, Error> {
2525        let mut info = self.chain_info_with_manager_values().await?;
2526        // If the current round has timed out, we request a timeout certificate and retry in
2527        // the next round.
2528        if let Some(round_timeout) = info.manager.round_timeout {
2529            if round_timeout <= self.storage_client().clock().current_time() {
2530                if let Err(e) = self.request_leader_timeout().await {
2531                    debug!("Failed to obtain a timeout certificate: {}", e);
2532                } else {
2533                    info = self.chain_info_with_manager_values().await?;
2534                }
2535            }
2536        }
2537        Ok(info)
2538    }
2539
2540    /// Finalizes the locking block.
2541    ///
2542    /// Panics if there is no locking block; fails if the locking block is not in the current round.
2543    async fn finalize_locking_block(
2544        &self,
2545        info: Box<ChainInfo>,
2546    ) -> Result<ClientOutcome<Option<ConfirmedBlockCertificate>>, Error> {
2547        let locking = info
2548            .manager
2549            .requested_locking
2550            .expect("Should have a locking block");
2551        let LockingBlock::Regular(certificate) = *locking else {
2552            panic!("Should have a locking validated block");
2553        };
2554        debug!(
2555            round = %certificate.round,
2556            "Finalizing locking block"
2557        );
2558        let committee = self.local_committee().await?;
2559        let certificate = self
2560            .client
2561            .finalize_block(&committee, certificate.clone())
2562            .await?;
2563        let certificate = self.client.storage_client().cache_certificate(certificate);
2564        self.update_validators(Some(&committee), Some(certificate.clone()))
2565            .await?;
2566        Ok(ClientOutcome::Committed(Some(CacheArc::unwrap_or_clone(
2567            certificate,
2568        ))))
2569    }
2570
2571    /// Returns the number for the round number oracle to use when staging a block proposal.
2572    async fn round_for_oracle(
2573        &self,
2574        info: &ChainInfo,
2575        identity: &AccountOwner,
2576    ) -> Result<Option<u32>, Error> {
2577        // Pretend we do use oracles: If we don't, the round number is never read anyway.
2578        match self.round_for_new_proposal(info, identity, true).await {
2579            // If it is a multi-leader round, use its number for the oracle.
2580            Ok(Either::Left(round)) => Ok(round.multi_leader()),
2581            // If there is no suitable round with oracles, use None: If it works without oracles,
2582            // the block won't read the value. If it returns a timeout, it will be a single-leader
2583            // round, in which the oracle returns None.
2584            Err(Error::BlockProposalError(_)) | Ok(Either::Right(_)) => Ok(None),
2585            Err(err) => Err(err),
2586        }
2587    }
2588
2589    /// Returns a round in which we can propose a new block or the given one, if possible.
2590    async fn round_for_new_proposal(
2591        &self,
2592        info: &ChainInfo,
2593        identity: &AccountOwner,
2594        has_oracle_responses: bool,
2595    ) -> Result<Either<Round, RoundTimeout>, Error> {
2596        let manager = &info.manager;
2597        let seed = manager.seed;
2598        // If there is a conflicting proposal in the current round, we can only propose if the
2599        // next round can be started without a timeout, i.e. if we are in a multi-leader round.
2600        // Similarly, we cannot propose a block that uses oracles in the fast round, and also
2601        // skip the fast round if fast blocks are not allowed.
2602        let skip_fast = manager.current_round.is_fast()
2603            && (has_oracle_responses || !self.options.allow_fast_blocks);
2604        let conflict = manager
2605            .requested_signed_proposal
2606            .as_ref()
2607            .into_iter()
2608            .chain(&manager.requested_proposed)
2609            .any(|proposal| proposal.content.round == manager.current_round)
2610            || skip_fast;
2611        let round = if !conflict {
2612            manager.current_round
2613        } else if let Some(round) = manager
2614            .ownership
2615            .next_round(manager.current_round)
2616            .filter(|_| manager.current_round.is_multi_leader() || manager.current_round.is_fast())
2617        {
2618            round
2619        } else if let Some(timeout) = info.round_timeout() {
2620            return Ok(Either::Right(timeout));
2621        } else {
2622            return Err(Error::BlockProposalError(
2623                "Conflicting proposal in the current round",
2624            ));
2625        };
2626        let current_committee = self
2627            .local_committee()
2628            .await?
2629            .validators
2630            .values()
2631            .map(|v| (AccountOwner::from(v.account_public_key), v.votes))
2632            .collect();
2633        if manager.should_propose(identity, round, seed, &current_committee) {
2634            return Ok(Either::Left(round));
2635        }
2636        if let Some(timeout) = info.round_timeout() {
2637            return Ok(Either::Right(timeout));
2638        }
2639        Err(Error::BlockProposalError(
2640            "Not a leader in the current round",
2641        ))
2642    }
2643
2644    /// Discards the pending block proposal, if any, so that a fresh block can be
2645    /// proposed instead of retrying the queued one.
2646    ///
2647    /// Note that this does not update the copy persisted in the wallet (callers that
2648    /// persist pending proposals should refresh it afterwards). Also, this should never
2649    /// be used on a proposal already submitted in the fast round.
2650    #[instrument(level = "trace")]
2651    pub async fn clear_pending_proposal(&self) {
2652        *self.proposal_mutex().lock().await = None;
2653    }
2654
2655    /// Rotates the key of the chain.
2656    ///
2657    /// Replaces current owners of the chain with the new key pair.
2658    #[cfg(with_testing)]
2659    #[instrument(level = "trace")]
2660    pub async fn rotate_key_pair(
2661        &self,
2662        public_key: linera_base::crypto::AccountPublicKey,
2663    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2664        self.transfer_ownership(public_key.into()).await
2665    }
2666
2667    /// Transfers ownership of the chain to a single super owner.
2668    #[instrument(level = "trace")]
2669    pub async fn transfer_ownership(
2670        &self,
2671        new_owner: AccountOwner,
2672    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2673        self.execute_operation(SystemOperation::ChangeOwnership {
2674            super_owners: vec![new_owner],
2675            owners: Vec::new(),
2676            first_leader: None,
2677            multi_leader_rounds: 5,
2678            open_multi_leader_rounds: false,
2679            timeout_config: TimeoutConfig::default(),
2680        })
2681        .await
2682    }
2683
2684    /// Adds another owner to the chain, and turns existing super owners into regular owners.
2685    #[instrument(level = "trace")]
2686    pub async fn share_ownership(
2687        &self,
2688        new_owner: AccountOwner,
2689        new_weight: u64,
2690    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2691        let ownership = self.prepare_chain().await?.manager.ownership;
2692        ensure!(
2693            ownership.is_active(),
2694            ChainError::InactiveChain(self.chain_id)
2695        );
2696        let mut owners = ownership.owners.into_iter().collect::<Vec<_>>();
2697        owners.extend(ownership.super_owners.into_iter().zip(iter::repeat(100)));
2698        owners.push((new_owner, new_weight));
2699        let operations = vec![Operation::system(SystemOperation::ChangeOwnership {
2700            super_owners: Vec::new(),
2701            owners,
2702            first_leader: ownership.first_leader,
2703            multi_leader_rounds: ownership.multi_leader_rounds,
2704            open_multi_leader_rounds: ownership.open_multi_leader_rounds,
2705            timeout_config: ownership.timeout_config,
2706        })];
2707        self.execute_block(operations, vec![]).await
2708    }
2709
2710    /// Returns the current ownership settings on this chain.
2711    #[instrument(level = "trace")]
2712    pub async fn query_chain_ownership(&self) -> Result<ChainOwnership, Error> {
2713        Ok(self
2714            .client
2715            .local_node
2716            .chain_state_view(self.chain_id)
2717            .await?
2718            .execution_state
2719            .system
2720            .ownership
2721            .get()
2722            .await?
2723            .clone())
2724    }
2725
2726    /// Changes the ownership of this chain. Fails if it would remove existing owners, unless
2727    /// `remove_owners` is `true`.
2728    #[instrument(level = "trace")]
2729    pub async fn change_ownership(
2730        &self,
2731        ownership: ChainOwnership,
2732    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2733        self.execute_operation(SystemOperation::ChangeOwnership {
2734            super_owners: ownership.super_owners.into_iter().collect(),
2735            owners: ownership.owners.into_iter().collect(),
2736            first_leader: ownership.first_leader,
2737            multi_leader_rounds: ownership.multi_leader_rounds,
2738            open_multi_leader_rounds: ownership.open_multi_leader_rounds,
2739            timeout_config: ownership.timeout_config.clone(),
2740        })
2741        .await
2742    }
2743
2744    /// Returns the current application permissions on this chain.
2745    #[instrument(level = "trace")]
2746    pub async fn query_application_permissions(&self) -> Result<ApplicationPermissions, Error> {
2747        Ok(self
2748            .client
2749            .local_node
2750            .chain_state_view(self.chain_id)
2751            .await?
2752            .execution_state
2753            .system
2754            .application_permissions
2755            .get()
2756            .await?
2757            .clone())
2758    }
2759
2760    /// Changes the application permissions configuration on this chain.
2761    #[instrument(level = "trace", skip(application_permissions))]
2762    pub async fn change_application_permissions(
2763        &self,
2764        application_permissions: ApplicationPermissions,
2765    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2766        self.execute_operation(SystemOperation::ChangeApplicationPermissions(
2767            application_permissions,
2768        ))
2769        .await
2770    }
2771
2772    /// Opens a new chain with a derived UID, crediting `balance` to `account` on it.
2773    #[instrument(level = "trace", skip(self))]
2774    pub async fn open_chain(
2775        &self,
2776        ownership: ChainOwnership,
2777        application_permissions: ApplicationPermissions,
2778        account: AccountOwner,
2779        balance: Amount,
2780    ) -> Result<ClientOutcome<(ChainDescription, ConfirmedBlockCertificate)>, Error> {
2781        // Check if we have a key for any owner before consuming ownership.
2782        let mut has_key = false;
2783        for owner in ownership.all_owners() {
2784            if self.has_key_for(owner).await? {
2785                has_key = true;
2786                break;
2787            }
2788        }
2789        let config = OpenChainConfig {
2790            ownership,
2791            account,
2792            balance,
2793            application_permissions,
2794        };
2795        let operation = Operation::system(SystemOperation::OpenChain(config));
2796        let certificate = match self.execute_block(vec![operation], vec![]).await? {
2797            ClientOutcome::Committed(certificate) => certificate,
2798            ClientOutcome::Conflict(certificate) => {
2799                return Ok(ClientOutcome::Conflict(certificate));
2800            }
2801            ClientOutcome::WaitForTimeout(timeout) => {
2802                return Ok(ClientOutcome::WaitForTimeout(timeout));
2803            }
2804        };
2805        // The only operation, i.e. the last transaction, created the new chain.
2806        let chain_blob = certificate
2807            .block()
2808            .body
2809            .blobs
2810            .last()
2811            .and_then(|blobs| blobs.last())
2812            .ok_or_else(|| Error::InternalError("Failed to create a new chain"))?;
2813        let description = bcs::from_bytes::<ChainDescription>(chain_blob.bytes())?;
2814        // If we have a key for any owner, add it to the list of tracked chains.
2815        if has_key {
2816            self.client
2817                .extend_chain_mode(description.id(), ListeningMode::FullChain);
2818            self.client
2819                .retry_pending_cross_chain_requests(self.chain_id)
2820                .await?;
2821        }
2822        Ok(ClientOutcome::Committed((description, certificate)))
2823    }
2824
2825    /// Publishes a checkpoint of the chain's execution state. The resulting block
2826    /// contains a single `SystemOperation::Checkpoint`; future nodes can bootstrap
2827    /// from the published blob instead of replaying the chain's history.
2828    #[instrument(level = "trace")]
2829    pub async fn checkpoint(&self) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2830        self.execute_operation(SystemOperation::Checkpoint).await
2831    }
2832
2833    /// Closes the chain (and loses everything in it!!).
2834    /// Returns `None` if the chain was already closed.
2835    #[instrument(level = "trace")]
2836    pub async fn close_chain(
2837        &self,
2838    ) -> Result<ClientOutcome<Option<ConfirmedBlockCertificate>>, Error> {
2839        match self.execute_operation(SystemOperation::CloseChain).await {
2840            Ok(outcome) => Ok(outcome.map(Some)),
2841            Err(Error::LocalNodeError(LocalNodeError::WorkerError(WorkerError::ChainError(
2842                chain_error,
2843            )))) if matches!(*chain_error, ChainError::ClosedChain) => {
2844                Ok(ClientOutcome::Committed(None)) // Chain is already closed.
2845            }
2846            Err(error) => Err(error),
2847        }
2848    }
2849
2850    /// Publishes some module, optionally along with a BCS-encoded `Formats`
2851    /// description that becomes a third blob alongside contract and service.
2852    #[cfg(not(target_arch = "wasm32"))]
2853    #[instrument(level = "trace", skip(contract, service, formats))]
2854    pub async fn publish_module(
2855        &self,
2856        contract: Bytecode,
2857        service: Bytecode,
2858        vm_runtime: VmRuntime,
2859        formats: Option<Vec<u8>>,
2860    ) -> Result<ClientOutcome<(ModuleId, ConfirmedBlockCertificate)>, Error> {
2861        let (blobs, module_id) =
2862            super::create_bytecode_blobs(contract, service, vm_runtime, formats).await;
2863        self.publish_module_blobs(blobs, module_id).await
2864    }
2865
2866    /// Publishes some module.
2867    #[cfg(not(target_arch = "wasm32"))]
2868    #[instrument(level = "trace", skip(blobs, module_id))]
2869    pub async fn publish_module_blobs(
2870        &self,
2871        blobs: Vec<Blob>,
2872        module_id: ModuleId,
2873    ) -> Result<ClientOutcome<(ModuleId, ConfirmedBlockCertificate)>, Error> {
2874        self.execute_operations(
2875            vec![Operation::system(SystemOperation::PublishModule {
2876                module_id,
2877            })],
2878            blobs,
2879        )
2880        .await?
2881        .try_map(|certificate| Ok((module_id, certificate)))
2882    }
2883
2884    /// Publishes some data blobs.
2885    #[instrument(level = "trace", skip(bytes))]
2886    pub async fn publish_data_blobs(
2887        &self,
2888        bytes: Vec<Vec<u8>>,
2889    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2890        let blobs = bytes.into_iter().map(Blob::new_data);
2891        let publish_blob_operations = blobs
2892            .clone()
2893            .map(|blob| {
2894                Operation::system(SystemOperation::PublishDataBlob {
2895                    blob_hash: blob.id().hash,
2896                })
2897            })
2898            .collect();
2899        self.execute_operations(publish_blob_operations, blobs.collect())
2900            .await
2901    }
2902
2903    /// Publishes some data blob.
2904    #[instrument(level = "trace", skip(bytes))]
2905    pub async fn publish_data_blob(
2906        &self,
2907        bytes: Vec<u8>,
2908    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2909        self.publish_data_blobs(vec![bytes]).await
2910    }
2911
2912    /// Creates an application by instantiating some bytecode.
2913    #[instrument(
2914        level = "trace",
2915        skip(self, parameters, instantiation_argument, required_application_ids)
2916    )]
2917    pub async fn create_application<
2918        A: Abi,
2919        Parameters: Serialize,
2920        InstantiationArgument: Serialize,
2921    >(
2922        &self,
2923        module_id: ModuleId<A, Parameters, InstantiationArgument>,
2924        parameters: &Parameters,
2925        instantiation_argument: &InstantiationArgument,
2926        required_application_ids: Vec<ApplicationId>,
2927    ) -> Result<ClientOutcome<(ApplicationId<A>, ConfirmedBlockCertificate)>, Error> {
2928        let instantiation_argument = serde_json::to_vec(instantiation_argument)?;
2929        let parameters = serde_json::to_vec(parameters)?;
2930        Ok(self
2931            .create_application_untyped(
2932                module_id.forget_abi(),
2933                parameters,
2934                instantiation_argument,
2935                required_application_ids,
2936            )
2937            .await?
2938            .map(|(app_id, cert)| (app_id.with_abi(), cert)))
2939    }
2940
2941    /// Creates an application by instantiating some bytecode.
2942    #[instrument(
2943        level = "trace",
2944        skip(
2945            self,
2946            module_id,
2947            parameters,
2948            instantiation_argument,
2949            required_application_ids
2950        )
2951    )]
2952    pub async fn create_application_untyped(
2953        &self,
2954        module_id: ModuleId,
2955        parameters: Vec<u8>,
2956        instantiation_argument: Vec<u8>,
2957        required_application_ids: Vec<ApplicationId>,
2958    ) -> Result<ClientOutcome<(ApplicationId, ConfirmedBlockCertificate)>, Error> {
2959        self.execute_operation(SystemOperation::CreateApplication {
2960            module_id,
2961            parameters,
2962            instantiation_argument,
2963            required_application_ids,
2964        })
2965        .await?
2966        .try_map(|certificate| {
2967            // The first message of the only operation created the application.
2968            let mut creation = certificate
2969                .block()
2970                .created_blob_ids()
2971                .into_iter()
2972                .filter(|blob_id| blob_id.blob_type == BlobType::ApplicationDescription)
2973                .collect::<Vec<_>>();
2974            if creation.len() > 1 {
2975                return Err(Error::InternalError(
2976                    "Unexpected number of application descriptions published",
2977                ));
2978            }
2979            let blob_id = creation.pop().ok_or(Error::InternalError(
2980                "ApplicationDescription blob not found.",
2981            ))?;
2982            let id = ApplicationId::new(blob_id.hash);
2983            Ok((id, certificate))
2984        })
2985    }
2986
2987    /// Creates a new committee and starts using it (admin chains only).
2988    #[instrument(level = "trace", skip(committee))]
2989    pub async fn stage_new_committee(
2990        &self,
2991        committee: Committee,
2992    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
2993        let blob = Blob::new(BlobContent::new_committee(bcs::to_bytes(&committee)?));
2994        let blob_hash = blob.id().hash;
2995        match self
2996            .execute_operations(
2997                vec![Operation::system(SystemOperation::Admin(
2998                    AdminOperation::PublishCommitteeBlob { blob_hash },
2999                ))],
3000                vec![blob],
3001            )
3002            .await?
3003        {
3004            ClientOutcome::Committed(_) => {}
3005            outcome @ ClientOutcome::WaitForTimeout(_) | outcome @ ClientOutcome::Conflict(_) => {
3006                return Ok(outcome)
3007            }
3008        }
3009        let epoch = self.chain_info().await?.epoch.try_add_one()?;
3010        self.execute_operation(SystemOperation::Admin(AdminOperation::CreateCommittee {
3011            epoch,
3012            blob_hash,
3013        }))
3014        .await
3015    }
3016
3017    /// Synchronizes the chain with the validators and creates blocks without any operations to
3018    /// process all incoming messages. This may require several blocks.
3019    ///
3020    /// If not all certificates could be processed due to a timeout, the timestamp for when to retry
3021    /// is returned, too.
3022    #[instrument(level = "trace")]
3023    pub async fn process_inbox(
3024        &self,
3025    ) -> Result<(Vec<ConfirmedBlockCertificate>, Option<RoundTimeout>), Error> {
3026        self.prepare_chain().await?;
3027        self.process_inbox_without_prepare().await
3028    }
3029
3030    /// Creates blocks without any operations to process all incoming messages. This may require
3031    /// several blocks.
3032    ///
3033    /// If not all certificates could be processed due to a timeout, the timestamp for when to retry
3034    /// is returned, too.
3035    #[instrument(level = "trace")]
3036    pub async fn process_inbox_without_prepare(
3037        &self,
3038    ) -> Result<(Vec<ConfirmedBlockCertificate>, Option<RoundTimeout>), Error> {
3039        #[cfg(with_metrics)]
3040        let _latency = super::metrics::PROCESS_INBOX_WITHOUT_PREPARE_LATENCY.measure_latency();
3041
3042        let mut certificates = Vec::new();
3043        loop {
3044            // We provide no operations - this means that the only operations executed
3045            // will be epoch changes, receiving messages and processing event stream
3046            // updates, if any are pending.
3047            match self.execute_block(vec![], vec![]).await {
3048                Ok(ClientOutcome::Committed(certificate)) => certificates.push(certificate),
3049                Ok(ClientOutcome::Conflict(certificate)) => certificates.push(*certificate),
3050                Ok(ClientOutcome::WaitForTimeout(timeout)) => {
3051                    return Ok((certificates, Some(timeout)));
3052                }
3053                // Nothing in the inbox and no stream updates to be processed.
3054                Err(Error::LocalNodeError(LocalNodeError::WorkerError(
3055                    WorkerError::ChainError(chain_error),
3056                ))) if matches!(*chain_error, ChainError::EmptyBlock) => {
3057                    return Ok((certificates, None));
3058                }
3059                Err(error) => return Err(error),
3060            };
3061        }
3062    }
3063
3064    /// Returns an operation to process the next pending new epoch, if there is one.
3065    ///
3066    /// Later epochs are not included, since a block may advance the epoch at most once;
3067    /// they are processed by subsequent blocks.
3068    async fn next_epoch_change(&self) -> Result<Option<Operation>, Error> {
3069        let next_epoch = self.chain_info().await?.epoch.try_add_one()?;
3070        if !self
3071            .has_admin_event(EPOCH_STREAM_NAME, next_epoch.0)
3072            .await?
3073        {
3074            return Ok(None);
3075        }
3076        Ok(Some(Operation::system(SystemOperation::ProcessNewEpoch(
3077            next_epoch,
3078        ))))
3079    }
3080
3081    /// Returns whether the system event on the admin chain with the given stream name and key
3082    /// exists in storage.
3083    async fn has_admin_event(&self, stream_name: &[u8], index: u32) -> Result<bool, Error> {
3084        let event_id = EventId {
3085            chain_id: self.client.admin_chain_id,
3086            stream_id: StreamId::system(stream_name),
3087            index,
3088        };
3089        Ok(self
3090            .client
3091            .storage_client()
3092            .read_event(event_id)
3093            .await?
3094            .is_some())
3095    }
3096
3097    /// Returns the indices and events from the storage
3098    pub async fn events_from_index(
3099        &self,
3100        stream_id: StreamId,
3101        start_index: u32,
3102    ) -> Result<Vec<IndexAndEvent>, Error> {
3103        Ok(self
3104            .client
3105            .storage_client()
3106            .read_events_from_index(&self.chain_id, &stream_id, start_index)
3107            .await?)
3108    }
3109
3110    /// Deprecates all configurations of voting rights up to the given one (admin chains only).
3111    /// Emits a `RemoveCommittee` event for every still-active epoch up to and including
3112    /// `revoked_epoch`.
3113    #[instrument(level = "trace")]
3114    pub async fn revoke_epochs(
3115        &self,
3116        revoked_epoch: Epoch,
3117    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
3118        self.prepare_chain().await?;
3119        let current_epoch = self.chain_info().await?.epoch;
3120        ensure!(
3121            revoked_epoch < current_epoch,
3122            Error::CannotRevokeCurrentEpoch(current_epoch)
3123        );
3124        let mut operations = Vec::new();
3125        for epoch_index in 0..=revoked_epoch.0 {
3126            let epoch = Epoch(epoch_index);
3127            if self
3128                .has_admin_event(REMOVED_EPOCH_STREAM_NAME, epoch.0)
3129                .await?
3130            {
3131                continue;
3132            }
3133            operations.push(Operation::system(SystemOperation::Admin(
3134                AdminOperation::RemoveCommittee { epoch },
3135            )));
3136        }
3137        ensure!(!operations.is_empty(), Error::EpochAlreadyRevoked);
3138        self.execute_operations(operations, vec![]).await
3139    }
3140
3141    /// Sends money to a chain.
3142    /// Do not check balance. (This may block the client)
3143    /// Do not confirm the transaction.
3144    #[cfg(with_testing)]
3145    #[instrument(level = "trace")]
3146    pub async fn transfer_to_account_unsafe_unconfirmed(
3147        &self,
3148        owner: AccountOwner,
3149        amount: Amount,
3150        recipient: Account,
3151    ) -> Result<ClientOutcome<ConfirmedBlockCertificate>, Error> {
3152        self.execute_operation(SystemOperation::Transfer {
3153            owner,
3154            recipient,
3155            amount,
3156        })
3157        .await
3158    }
3159
3160    /// Reads the confirmed block with the given hash from storage.
3161    #[instrument(level = "trace", skip(hash))]
3162    pub async fn read_confirmed_block(
3163        &self,
3164        hash: CryptoHash,
3165    ) -> Result<Arc<ConfirmedBlock>, Error> {
3166        self.client
3167            .storage_client()
3168            .read_confirmed_block(hash)
3169            .await?
3170            .ok_or(Error::MissingConfirmedBlock(hash))
3171            .map(|b| b.into_std())
3172    }
3173
3174    /// Reads the confirmed block certificate with the given hash from storage.
3175    #[instrument(level = "trace", skip(hash))]
3176    pub async fn read_certificate(
3177        &self,
3178        hash: CryptoHash,
3179    ) -> Result<CacheArc<ConfirmedBlockCertificate>, Error> {
3180        self.client
3181            .storage_client()
3182            .read_certificate(hash)
3183            .await?
3184            .ok_or(Error::ReadCertificatesError(vec![hash]))
3185    }
3186
3187    /// Handles any cross-chain requests for any pending outgoing messages.
3188    #[instrument(level = "trace")]
3189    pub async fn retry_pending_outgoing_messages(&self) -> Result<(), Error> {
3190        self.client
3191            .retry_pending_cross_chain_requests(self.chain_id)
3192            .await?;
3193        Ok(())
3194    }
3195
3196    #[instrument(level = "trace", skip(local_node))]
3197    async fn maybe_local_chain_info(
3198        &self,
3199        chain_id: ChainId,
3200        local_node: &LocalNodeClient<Env::Storage>,
3201    ) -> Result<Option<Box<ChainInfo>>, Error> {
3202        match local_node.chain_info(chain_id).await {
3203            Ok(info) => Ok(Some(info)),
3204            Err(LocalNodeError::BlobsNotFound(_) | LocalNodeError::InactiveChain(_)) => Ok(None),
3205            Err(err) => Err(err.into()),
3206        }
3207    }
3208
3209    #[instrument(level = "trace", skip(chain_id, local_node))]
3210    async fn local_next_block_height(
3211        &self,
3212        chain_id: ChainId,
3213        local_node: &LocalNodeClient<Env::Storage>,
3214    ) -> Result<BlockHeight, Error> {
3215        Ok(self
3216            .maybe_local_chain_info(chain_id, local_node)
3217            .await?
3218            .map_or(BlockHeight::ZERO, |info| info.next_block_height))
3219    }
3220
3221    /// Returns the next height we expect to receive from the given sender chain, according to the
3222    /// local inbox.
3223    #[instrument(level = "trace")]
3224    async fn local_next_height_to_receive(&self, origin: ChainId) -> Result<BlockHeight, Error> {
3225        Ok(self
3226            .client
3227            .local_node
3228            .get_inbox_next_height(self.chain_id, origin)
3229            .await?)
3230    }
3231
3232    #[instrument(level = "trace", skip(remote_node, local_node, notification))]
3233    async fn process_notification(
3234        &self,
3235        remote_node: RemoteNode<Env::ValidatorNode>,
3236        local_node: LocalNodeClient<Env::Storage>,
3237        notification: Notification,
3238    ) -> Result<(), Error> {
3239        let listening_mode = self.client.chain_mode(notification.chain_id);
3240        let relevant = listening_mode
3241            .as_ref()
3242            .is_some_and(|mode| mode.is_relevant(&notification.reason));
3243        if !relevant {
3244            tracing::trace!(
3245                chain_id = %notification.chain_id,
3246                reason = ?notification.reason,
3247                ?listening_mode,
3248                "Ignoring notification due to listening mode"
3249            );
3250            return Ok(());
3251        }
3252        match notification.reason {
3253            Reason::NewIncomingBundle { origin, height } => {
3254                if self.options.message_policy.ignores_origin(&origin) {
3255                    trace!(
3256                        chain_id = %self.chain_id,
3257                        %origin,
3258                        %height,
3259                        "Skipping NewIncomingBundle notification: origin filtered by message_policy"
3260                    );
3261                    return Ok(());
3262                }
3263                if self.local_next_height_to_receive(origin).await? > height {
3264                    debug!(
3265                        chain_id = %self.chain_id,
3266                        "Accepting redundant notification for new message"
3267                    );
3268                    return Ok(());
3269                }
3270                self.client
3271                    .download_sender_block_with_sending_ancestors(
3272                        self.chain_id,
3273                        origin,
3274                        height,
3275                        &remote_node,
3276                    )
3277                    .await?;
3278                if self.local_next_height_to_receive(origin).await? <= height {
3279                    info!(
3280                        chain_id = %self.chain_id,
3281                        "NewIncomingBundle: Fail to synchronize new message after notification"
3282                    );
3283                }
3284            }
3285            Reason::NewBlock { height, .. } => {
3286                let chain_id = notification.chain_id;
3287                let local_height = self.local_next_block_height(chain_id, &local_node).await?;
3288                if local_height > height {
3289                    debug!(
3290                        chain_id = %self.chain_id,
3291                        "Accepting redundant notification for new block"
3292                    );
3293                    return Ok(());
3294                }
3295                // The below will only happen in modes other than EventsOnly, as the
3296                // NewBlock notification is only relevant to other modes.
3297                self.client
3298                    .synchronize_chain_state_from(&remote_node, chain_id)
3299                    .await?;
3300                if self.local_next_block_height(chain_id, &local_node).await? <= height {
3301                    error!("NewBlock: Fail to synchronize new block after notification");
3302                }
3303                trace!(
3304                    chain_id = %self.chain_id,
3305                    %height,
3306                    "NewBlock: processed notification",
3307                );
3308            }
3309            Reason::NewEvents {
3310                height, block_hash, ..
3311            } => {
3312                let chain_id = notification.chain_id;
3313                let local_height = self.local_next_block_height(chain_id, &local_node).await?;
3314                if local_height > height {
3315                    debug!(
3316                        chain_id = %self.chain_id,
3317                        "Accepting redundant notification for new events"
3318                    );
3319                    return Ok(());
3320                }
3321                trace!(
3322                    chain_id = %self.chain_id,
3323                    %height,
3324                    "NewEvents: processing notification"
3325                );
3326                // Use the subscribed streams from EventsOnly mode to figure out which
3327                // streams we are interested in.
3328                let relevant_streams = match self.listening_mode() {
3329                    Some(ListeningMode::EventsOnly(subscribed)) => subscribed,
3330                    // other cases should be unreachable, as the NewEvents notification is
3331                    // only relevant in the EventsOnly mode
3332                    _ => unreachable!(),
3333                };
3334                self.client
3335                    .download_event_bearing_blocks(
3336                        self.chain_id,
3337                        BTreeSet::from([(height, block_hash)]),
3338                        local_height,
3339                        &relevant_streams,
3340                        &remote_node,
3341                    )
3342                    .await?;
3343            }
3344            Reason::NewRound { height, round } => {
3345                let chain_id = notification.chain_id;
3346                if let Some(info) = self.maybe_local_chain_info(chain_id, &local_node).await? {
3347                    if (info.next_block_height, info.manager.current_round) >= (height, round) {
3348                        debug!(
3349                            chain_id = %self.chain_id,
3350                            "Accepting redundant notification for new round"
3351                        );
3352                        return Ok(());
3353                    }
3354                }
3355                self.client
3356                    .synchronize_chain_state_from(&remote_node, chain_id)
3357                    .await?;
3358                let Some(info) = self.maybe_local_chain_info(chain_id, &local_node).await? else {
3359                    error!(
3360                        chain_id = %self.chain_id,
3361                        "NewRound: Fail to read local chain info for {chain_id}"
3362                    );
3363                    return Ok(());
3364                };
3365                if (info.next_block_height, info.manager.current_round) < (height, round) {
3366                    info!(
3367                        chain_id = %self.chain_id,
3368                        "NewRound: Fail to synchronize new block after notification"
3369                    );
3370                }
3371            }
3372            Reason::BlockExecuted { .. } => {
3373                // No action needed.
3374            }
3375        }
3376        Ok(())
3377    }
3378
3379    /// Returns whether this chain is tracked by the client, i.e. we are updating its inbox.
3380    pub fn is_tracked(&self) -> bool {
3381        self.client.is_tracked(self.chain_id)
3382    }
3383
3384    /// Returns the listening mode for this chain, if it is tracked.
3385    pub fn listening_mode(&self) -> Option<ListeningMode> {
3386        self.client.chain_mode(self.chain_id)
3387    }
3388
3389    /// Spawns a task that listens to notifications about the current chain from all validators,
3390    /// and synchronizes the local state accordingly.
3391    ///
3392    /// The listening mode must be set in `Client::chain_modes` before calling this method.
3393    #[instrument(level = "trace", fields(chain_id = ?self.chain_id))]
3394    pub async fn listen(
3395        &self,
3396    ) -> Result<(impl Future<Output = ()>, AbortOnDrop, NotificationStream), Error> {
3397        use future::FutureExt as _;
3398
3399        async fn await_while_polling<F: FusedFuture>(
3400            future: F,
3401            background_work: impl FusedStream<Item = ()>,
3402        ) -> F::Output {
3403            tokio::pin!(future);
3404            tokio::pin!(background_work);
3405            loop {
3406                futures::select! {
3407                    _ = background_work.next() => (),
3408                    result = future => return result,
3409                }
3410            }
3411        }
3412
3413        let mut senders: HashMap<ValidatorPublicKey, StreamHandle> = HashMap::new();
3414        let mut circuit_breakers: HashMap<ValidatorPublicKey, CircuitBreakerState> = HashMap::new();
3415        let notifications = self.subscribe()?;
3416        let (abortable_notifications, abort) = stream::abortable(self.subscribe()?);
3417
3418        // Beware: if this future ceases to make progress, notification processing will
3419        // deadlock, because of the issue described in
3420        // https://github.com/linera-io/linera-protocol/pull/1173.
3421
3422        // TODO(#2013): replace this lock with an asynchronous communication channel
3423
3424        let mut process_notifications = FuturesUnordered::new();
3425
3426        // A failed update here leaves no streams, no breakers and nothing to wake the
3427        // loop, and nothing retries `listen()` on failure — so the retry deadline has to
3428        // be armed from the start, exactly as the in-loop failure path does. (A chain
3429        // whose listening mode is later UPGRADED does get a fresh `listen()`, which
3430        // discards this listener's accumulated breaker state.)
3431        let mut retry_update_at: Option<Timestamp> = None;
3432        match self
3433            .update_notification_streams(&mut senders, &mut circuit_breakers)
3434            .await
3435        {
3436            Ok(tasks) => process_notifications.extend(tasks),
3437            Err(error) => {
3438                error!("Failed to update committee: {error}");
3439                retry_update_at = Some(self.retry_update_deadline());
3440            }
3441        };
3442
3443        let this = self.clone();
3444        // Boxed so that nesting this loop's futures does not overflow the trait solver
3445        // proving `Send` for callers that spawn it.
3446        let update_streams: MaybeSendBoxFuture<'static, ()> = Box::pin(
3447            async move {
3448                let mut abortable_notifications = abortable_notifications.fuse();
3449                // The probe timer, held across iterations so it is not rebuilt on every
3450                // notification; `armed_probe_at` is the deadline it currently encodes.
3451                let mut armed_probe_at: Option<Timestamp> = None;
3452                let probe_due = Either::Right(future::pending()).fuse();
3453                tokio::pin!(probe_due);
3454
3455                'listen: loop {
3456                    // Earliest deadline of any kind. Waking on it is what lets a chain
3457                    // with no live streams repair them: otherwise the update runs only on
3458                    // this chain's next `NewBlock`, which cannot arrive while every
3459                    // stream carrying it is dead.
3460                    let next_probe_at = circuit_breakers
3461                        .values()
3462                        .map(|state| state.next_probe_at)
3463                        .chain(retry_update_at)
3464                        .min();
3465                    // Rebuild the timer only when the deadline actually moves; a
3466                    // notification that does not trigger an update must not discard and
3467                    // recreate it, which under `TestClock` also strands a `oneshot` per
3468                    // iteration in the shared sleep map.
3469                    if next_probe_at != armed_probe_at {
3470                        armed_probe_at = next_probe_at;
3471                        probe_due.set(
3472                            match next_probe_at {
3473                                Some(deadline) => Either::Left(
3474                                    this.storage_client().clock().sleep_until(deadline),
3475                                ),
3476                                None => Either::Right(future::pending()),
3477                            }
3478                            .fuse(),
3479                        );
3480                    }
3481
3482                    let update_now = futures::select! {
3483                        maybe_notification = abortable_notifications.next() => {
3484                            match maybe_notification {
3485                                // Re-subscribe to validators on new blocks to handle
3486                                // committee changes.
3487                                Some(notification) => {
3488                                    matches!(notification.reason, Reason::NewBlock { .. })
3489                                }
3490                                None => break 'listen,
3491                            }
3492                        }
3493                        // A validator notification stream task finished: update now, so
3494                        // that the ended stream is registered in the circuit breaker and
3495                        // a probe gets scheduled even if this chain never produces
3496                        // another block.
3497                        _ = process_notifications.select_next_some() => {
3498                            // Drain the completions that are already ready first. The tasks
3499                            // are individual, so a simultaneous loss of every stream on this
3500                            // chain queues one per stream, and each would otherwise drive a
3501                            // full update of its own — an N-times amplification per chain at
3502                            // exactly the moment a fleet-wide event has hit every chain on
3503                            // the worker, with each update able to become a quorum
3504                            // `synchronize_chain_state()` through the `BlobsNotFound`
3505                            // fallback. One pass registers all of them.
3506                            while process_notifications.next().now_or_never().flatten().is_some() {}
3507                            true
3508                        }
3509                        // A circuit-breaker probe is due. Clear the armed deadline so
3510                        // the timer is rebuilt below: it is a `Fuse` that has now
3511                        // completed, and `select!` skips terminated arms in silence, so
3512                        // leaving it in place when the recomputed deadline happens to be
3513                        // unchanged would cost the listener its only timer wake-up.
3514                        _ = probe_due => {
3515                            armed_probe_at = None;
3516                            true
3517                        }
3518                    };
3519                    if !update_now {
3520                        continue;
3521                    }
3522                    // `await_while_polling` keeps draining the stream tasks while the
3523                    // update runs, and both of the update's await points precede its
3524                    // transition scan, so a stream that ends mid-update is still seen.
3525                    match Box::pin(await_while_polling(
3526                        this.update_notification_streams(&mut senders, &mut circuit_breakers)
3527                            .fuse(),
3528                        &mut process_notifications,
3529                    ))
3530                    .await
3531                    {
3532                        Ok(tasks) => {
3533                            retry_update_at = None;
3534                            process_notifications.extend(tasks);
3535                        }
3536                        Err(error) => {
3537                            error!("Failed to update committee: {error}");
3538                            // The update returns before touching any breaker, so every
3539                            // deadline it would have advanced is still in the past and
3540                            // would keep winning `min()` below, re-firing the timer into
3541                            // the same failing call. Push out only those — clamping the
3542                            // rest up to `retry` would postpone a probe that is genuinely
3543                            // due sooner, which is what taking the `min` exists to avoid.
3544                            let retry = this.retry_update_deadline();
3545                            this.defer_due_probes(&mut circuit_breakers, retry);
3546                            retry_update_at = Some(retry);
3547                        }
3548                    }
3549                }
3550
3551                for handle in senders.into_values() {
3552                    handle.abort.abort();
3553                }
3554
3555                let () = process_notifications.collect().await;
3556            }
3557            .in_current_span(),
3558        );
3559
3560        Ok((update_streams, AbortOnDrop(abort), notifications))
3561    }
3562
3563    /// The configured initial probe interval, floored.
3564    fn initial_probe_interval(&self) -> Duration {
3565        self.options
3566            .notification_circuit_breaker_initial_probe_interval
3567            .max(MIN_PROBE_INTERVAL)
3568    }
3569
3570    /// A stable per-validator offset, so that probes armed by one fleet-wide event stay
3571    /// spread out instead of re-converging at every doubling.
3572    ///
3573    /// Always a fraction of the INITIAL interval, never of the current escalated one — a
3574    /// parameter here would read as though the spread grows with the backoff, which would
3575    /// make a capped breaker's jitter larger than the gap it is jittering.
3576    fn probe_spread(&self, validator: &ValidatorPublicKey) -> Duration {
3577        let mut hasher = DefaultHasher::new();
3578        (self.chain_id, validator).hash(&mut hasher);
3579        (self.initial_probe_interval() / PROBE_SPREAD_DIVISOR)
3580            .mul_f64((hasher.finish() % 1_000) as f64 / 1_000.0)
3581    }
3582
3583    /// Pushes the already-due probe deadlines out to `retry`, after an update failed
3584    /// before it could touch any of them.
3585    ///
3586    /// Only the due ones: they are what would otherwise keep winning `min()` and re-fire
3587    /// the timer straight back into the same failing call. Clamping the rest up to `retry`
3588    /// as well would postpone a probe that is legitimately due sooner, which is exactly
3589    /// what taking the `min` over both deadlines exists to avoid.
3590    pub(super) fn defer_due_probes(
3591        &self,
3592        circuit_breakers: &mut HashMap<ValidatorPublicKey, CircuitBreakerState>,
3593        retry: Timestamp,
3594    ) {
3595        let now = self.storage_client().clock().current_time();
3596        for state in circuit_breakers.values_mut() {
3597            if state.next_probe_at <= now {
3598                state.next_probe_at = retry;
3599            }
3600        }
3601    }
3602
3603    /// When to retry an update that failed before it could touch the breaker state.
3604    fn retry_update_deadline(&self) -> Timestamp {
3605        self.storage_client()
3606            .clock()
3607            .current_time()
3608            .saturating_add(TimeDelta::from_duration(self.initial_probe_interval()))
3609    }
3610
3611    /// Creates or repairs the per-validator notification stream tasks for this chain,
3612    /// and updates the circuit-breaker state for validators whose streams have ended.
3613    /// Returns the newly created stream tasks; the caller is responsible for polling
3614    /// them.
3615    #[instrument(level = "trace", skip(senders, circuit_breakers))]
3616    pub(super) async fn update_notification_streams(
3617        &self,
3618        senders: &mut HashMap<ValidatorPublicKey, StreamHandle>,
3619        circuit_breakers: &mut HashMap<ValidatorPublicKey, CircuitBreakerState>,
3620    ) -> Result<Vec<MaybeSendBoxFuture<'static, ()>>, Error> {
3621        let initial_probe_interval = self.initial_probe_interval();
3622        // Floored by the initial interval rather than by `MIN_PROBE_INTERVAL`: sharing the
3623        // floor would let a `0` max pin both ends together and delete escalation entirely.
3624        let max_probe_interval = self
3625            .options
3626            .notification_circuit_breaker_max_probe_interval
3627            .max(initial_probe_interval);
3628        let (nodes, local_node) = {
3629            // For EventsOnly chains we may not have the chain's own committee locally,
3630            // and attempting to fetch it would trigger a full sync. Use the admin
3631            // committee instead — we only need it to know which validators to connect to.
3632            let committee = if self
3633                .listening_mode()
3634                .is_some_and(|m| m.should_sync_chain_state())
3635            {
3636                self.local_committee().await?
3637            } else {
3638                self.client.admin_committee().await?.1
3639            };
3640            let nodes = self
3641                .client
3642                .validator_node_provider()
3643                .make_nodes(&committee)?
3644                .collect::<HashMap<_, _>>();
3645            (nodes, self.client.local_node.clone())
3646        };
3647        // Read the (possibly simulated) node clock once, so circuit-breaker probe scheduling
3648        // can be driven deterministically in tests instead of depending on the wall clock.
3649        //
3650        // AFTER the committee read above, which is this function's only await and can be a
3651        // quorum round trip via `local_committee()`'s `BlobsNotFound` fallback. Sampling it
3652        // before would arm every deadline below at a moment already past by the time the
3653        // update returns — an unpaced retry on the `Ok` path, which has no `defer_due_probes`
3654        // — and would make `serving_for` saturate to zero for a stream that began serving
3655        // during the await, charging a healthy stream as a persistent fault.
3656        let now = self.storage_client().clock().current_time();
3657        // Detect circuit breaker state transitions before cleaning up senders.
3658        //
3659        // Health is judged on whether the stream reached the point of serving
3660        // notifications, not on the task still running: the probe body awaits the initial
3661        // sync before anything polls the stream, so "alive" on its own also covers a probe
3662        // that has delivered nothing and may never deliver anything.
3663        let mut abort_stalled_probes = Vec::new();
3664        for (validator, handle) in senders.iter() {
3665            if !nodes.contains_key(validator) {
3666                continue;
3667            }
3668            let died = handle.abort.is_aborted();
3669            let serving_for = handle.serving_for(now);
3670            match (died, serving_for) {
3671                // Serving for at least one interval and still running: healthy.
3672                //
3673                // The same threshold the churn arm uses, and for the same reason. Dropping
3674                // the breaker after ANY amount of serving would make the ladder depend on
3675                // whether an unrelated update happened to land inside a brief serving
3676                // window — the initial sync emits `NewBlock`, so on an active chain one
3677                // usually does — and a validator that had escalated to the cap would be
3678                // reset to the base cadence by a stream that served for seconds. Below the
3679                // threshold the breaker simply stays, and its deadline is in the future.
3680                (false, Some(lifetime)) if lifetime >= initial_probe_interval => {
3681                    // Drop the breaker either way so the timer goes quiet, but only report
3682                    // a recovery for one that actually tripped: every launch arms a
3683                    // deadline, so gating on the removal alone announces a recovery for
3684                    // every healthy validator on the first update after it starts serving.
3685                    if circuit_breakers
3686                        .remove(validator)
3687                        .is_some_and(|state| state.tripped)
3688                    {
3689                        info!(
3690                            %validator,
3691                            chain_id = %self.chain_id,
3692                            "Validator recovered from circuit breaker"
3693                        );
3694                    }
3695                }
3696                // Serving, but not yet for long enough to clear the breaker.
3697                //
3698                // Its deadline is NOT necessarily in the future: a probe arms at
3699                // `launch + probe_interval + spread` while the stream only starts serving
3700                // after its sync returns, so whenever that sync outlasts this validator's
3701                // jitter offset the timer fires here with the deadline already elapsed.
3702                // Leaving it elapsed would keep it winning `min()` in `listen()`, and
3703                // `sleep_until` on a past instant returns immediately — a full
3704                // `update_notification_streams` per pass until the lifetime finally
3705                // crosses the threshold. Re-arm at exactly the qualifying instant; the
3706                // interval and the ladder are untouched.
3707                (false, Some(lifetime)) => {
3708                    if let Some(state) = circuit_breakers.get_mut(validator) {
3709                        if now >= state.next_probe_at {
3710                            state.next_probe_at = now.saturating_add(TimeDelta::from_duration(
3711                                initial_probe_interval.saturating_sub(lifetime),
3712                            ));
3713                        }
3714                    }
3715                }
3716                // Still connecting or syncing when the deadline passed. Abort it, or its
3717                // `senders` entry stays occupied and the validator is never probed again.
3718                (false, None) => {
3719                    if let Some(state) = circuit_breakers.get_mut(validator) {
3720                        if now >= state.next_probe_at {
3721                            // A breaker that has not tripped is the deadline armed at
3722                            // launch, so this is the stream's FIRST recorded failure and
3723                            // belongs at the configured interval, not at twice it.
3724                            if state.tripped {
3725                                state.escalate(
3726                                    now,
3727                                    max_probe_interval,
3728                                    self.probe_spread(validator),
3729                                );
3730                            } else {
3731                                *state = CircuitBreakerState::failed(
3732                                    now,
3733                                    initial_probe_interval,
3734                                    self.probe_spread(validator),
3735                                );
3736                            }
3737                            abort_stalled_probes.push(*validator);
3738                            warn!(
3739                                %validator,
3740                                chain_id = %self.chain_id,
3741                                next_probe_in = ?state.probe_interval,
3742                                "Stream did not start serving in time; aborting and retrying later"
3743                            );
3744                        }
3745                    }
3746                }
3747                // Died after serving for at least one interval: the stream did real work
3748                // before stopping, so re-arm at the initial interval instead of doubling.
3749                // Below that threshold it is not churn — the transport already absorbs
3750                // reconnects internally, so a short-lived stream means a persistent fault.
3751                (true, Some(lifetime)) if lifetime >= initial_probe_interval => {
3752                    circuit_breakers.insert(
3753                        *validator,
3754                        CircuitBreakerState::churned(
3755                            now,
3756                            initial_probe_interval,
3757                            self.probe_spread(validator),
3758                        ),
3759                    );
3760                    info!(
3761                        %validator,
3762                        chain_id = %self.chain_id,
3763                        ?lifetime,
3764                        next_probe_in = ?initial_probe_interval,
3765                        "Long-lived validator notification stream ended; re-probing"
3766                    );
3767                }
3768                // Died without serving, or after too short a life to count as churn.
3769                (true, _) => {
3770                    // Escalate only a breaker that already tripped. Every launch arms one,
3771                    // so keying on presence alone charges a validator's FIRST stream death
3772                    // as a failed probe: the ladder starts at twice the configured
3773                    // interval and the error-level record that a stream was lost at all
3774                    // becomes unreachable.
3775                    if let Some(state) = circuit_breakers
3776                        .get_mut(validator)
3777                        .filter(|state| state.tripped)
3778                    {
3779                        state.escalate(now, max_probe_interval, self.probe_spread(validator));
3780                        warn!(
3781                            %validator,
3782                            chain_id = %self.chain_id,
3783                            next_probe_in = ?state.probe_interval,
3784                            "Validator still unhealthy after probe; increasing probe interval"
3785                        );
3786                    } else {
3787                        circuit_breakers.insert(
3788                            *validator,
3789                            CircuitBreakerState::failed(
3790                                now,
3791                                initial_probe_interval,
3792                                self.probe_spread(validator),
3793                            ),
3794                        );
3795                        error!(
3796                            %validator,
3797                            chain_id = %self.chain_id,
3798                            next_probe_in = ?initial_probe_interval,
3799                            "Validator notification stream ended; entering circuit breaker"
3800                        );
3801                    }
3802                }
3803            }
3804        }
3805        for validator in abort_stalled_probes {
3806            if let Some(handle) = senders.get(&validator) {
3807                handle.abort.abort();
3808            }
3809        }
3810
3811        senders.retain(|validator, handle| {
3812            if !nodes.contains_key(validator) {
3813                handle.abort.abort();
3814            }
3815            !handle.abort.is_aborted()
3816        });
3817        circuit_breakers.retain(|validator, _| nodes.contains_key(validator));
3818
3819        let mut validator_tasks: Vec<MaybeSendBoxFuture<'static, ()>> = Vec::new();
3820        for (public_key, node) in nodes {
3821            let hash_map::Entry::Vacant(entry) = senders.entry(public_key) else {
3822                continue;
3823            };
3824
3825            let spread = self.probe_spread(&public_key);
3826            match circuit_breakers.entry(public_key) {
3827                hash_map::Entry::Occupied(mut breaker) => {
3828                    let state = breaker.get_mut();
3829                    if now < state.next_probe_at {
3830                        continue;
3831                    }
3832                    // Re-arm before launching: the outcome is only known at a later
3833                    // update, and a deadline left in the past re-enters this loop at once.
3834                    state.arm(now, spread);
3835                    debug!(
3836                        validator = %public_key,
3837                        chain_id = %self.chain_id,
3838                        "Probing unhealthy validator"
3839                    );
3840                }
3841                // A validator's first launch. Arm a deadline anyway, or a `subscribe` or
3842                // sync that never returns leaves nothing to abort it and nothing to wake
3843                // the timer on its account. Recovery drops the breaker once it serves.
3844                hash_map::Entry::Vacant(breaker) => {
3845                    breaker.insert(CircuitBreakerState::launched(
3846                        now,
3847                        initial_probe_interval * FIRST_LAUNCH_GRACE,
3848                        spread,
3849                    ));
3850                }
3851            }
3852
3853            let address = node.address();
3854            let this = self.clone();
3855            let listening_mode_for_sync = self.listening_mode();
3856            let serving_since = Arc::new(AtomicU64::new(NOT_SERVING));
3857            let serving_since_for_task = serving_since.clone();
3858            let stream = stream::once({
3859                let node = node.clone();
3860                async move {
3861                    let stream = node.subscribe(vec![this.chain_id]).await?;
3862                    // The stream is established but nothing polls it until this block
3863                    // returns, so the sync below still has to finish before the validator
3864                    // counts as serving.
3865                    let remote_node = RemoteNode { public_key, node };
3866                    if listening_mode_for_sync
3867                        .as_ref()
3868                        .is_some_and(|mode| mode.should_sync_chain_state())
3869                    {
3870                        this.client
3871                            .synchronize_chain_state_from(&remote_node, this.chain_id)
3872                            .await?;
3873                    } else {
3874                        // For EventsOnly chains, do a lightweight initial sync:
3875                        // query the validator for the latest event-bearing blocks
3876                        // for our subscribed streams, then download them sparsely.
3877                        if let Some(ListeningMode::EventsOnly(subscribed)) =
3878                            listening_mode_for_sync.as_ref()
3879                        {
3880                            if let Err(error) = this
3881                                .client
3882                                .sync_events_from_node(this.chain_id, subscribed, &remote_node)
3883                                .await
3884                            {
3885                                debug!(
3886                                    chain_id = %this.chain_id,
3887                                    %error,
3888                                    "Failed initial sparse sync for EventsOnly chain"
3889                                );
3890                            }
3891                        }
3892                    }
3893                    serving_since_for_task.store(
3894                        this.storage_client().clock().current_time().micros(),
3895                        Ordering::Relaxed,
3896                    );
3897                    Ok::<_, Error>(stream)
3898                }
3899            })
3900            .filter_map(move |result| {
3901                let address = address.clone();
3902                async move {
3903                    if let Err(error) = &result {
3904                        info!(?error, address, "could not connect to validator");
3905                    } else {
3906                        debug!(address, "connected to validator");
3907                    }
3908                    result.ok()
3909                }
3910            })
3911            .flatten();
3912            let (stream, abort) = stream::abortable(stream);
3913            let mut stream = Box::pin(stream);
3914            let abort_on_exit = abort.clone();
3915            let this = self.clone();
3916            let local_node = local_node.clone();
3917            let remote_node = RemoteNode { public_key, node };
3918            validator_tasks.push(Box::pin(async move {
3919                while let Some(notification) = stream.next().await {
3920                    if let Err(error) = this
3921                        .process_notification(
3922                            remote_node.clone(),
3923                            local_node.clone(),
3924                            notification.clone(),
3925                        )
3926                        .await
3927                    {
3928                        tracing::info!(
3929                            chain_id = %this.chain_id,
3930                            address = remote_node.address(),
3931                            ?notification,
3932                            %error,
3933                            "failed to process notification",
3934                        );
3935                    }
3936                }
3937                warn!(
3938                    chain_id = %this.chain_id,
3939                    address = remote_node.address(),
3940                    "Validator notification stream ended"
3941                );
3942                abort_on_exit.abort();
3943            }));
3944            entry.insert(StreamHandle {
3945                abort,
3946                serving_since,
3947            });
3948        }
3949        Ok(validator_tasks)
3950    }
3951
3952    /// Attempts to update a validator with the local information: sends any confirmed
3953    /// block certificates the validator is missing, along with the blobs and other
3954    /// chains' blocks they depend on, and evidence for the current consensus round.
3955    ///
3956    /// The public key is used to verify the validator's responses.
3957    #[instrument(level = "trace", skip(node))]
3958    pub async fn sync_validator(
3959        &self,
3960        public_key: ValidatorPublicKey,
3961        node: Env::ValidatorNode,
3962    ) -> Result<(), Error> {
3963        let local_next_block_height = self.chain_info().await?.next_block_height;
3964        let mut updater = self
3965            .client
3966            .remote_node_updater(RemoteNode { public_key, node });
3967        updater
3968            .send_chain_information(
3969                self.chain_id,
3970                local_next_block_height,
3971                CrossChainMessageDelivery::NonBlocking,
3972                None,
3973            )
3974            .await
3975    }
3976}
3977
3978#[cfg(with_testing)]
3979impl<Env: Environment> ChainClient<Env> {
3980    /// Processes a notification received from the given validator.
3981    pub async fn process_notification_from(
3982        &self,
3983        notification: Notification,
3984        validator: (ValidatorPublicKey, &str),
3985    ) {
3986        let mut node_list = self
3987            .client
3988            .validator_node_provider()
3989            .make_nodes_from_list(vec![validator])
3990            .unwrap();
3991        let (public_key, node) = node_list.next().unwrap();
3992        let remote_node = RemoteNode { node, public_key };
3993        let local_node = self.client.local_node.clone();
3994        self.process_notification(remote_node, local_node, notification)
3995            .await
3996            .unwrap();
3997    }
3998}
3999
4000#[cfg(test)]
4001mod tests {
4002    use super::{Error, LocalNodeError};
4003
4004    #[test]
4005    fn error_type_delegates_to_local_node_error() {
4006        assert_eq!(
4007            Error::LocalNodeError(LocalNodeError::InvalidChainInfoResponse).error_type(),
4008            "LocalNodeError::InvalidChainInfoResponse"
4009        );
4010    }
4011
4012    #[test]
4013    fn error_type_falls_back_to_chain_client_variant() {
4014        assert_eq!(
4015            Error::WalletSynchronizationError.error_type(),
4016            "ChainClientError::WalletSynchronizationError"
4017        );
4018    }
4019}