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