Skip to main content

linera_core/client/
mod.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2// Copyright (c) Zefchain Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    cmp::{Ordering, Reverse},
7    collections::{BTreeMap, BTreeSet, HashSet},
8    slice,
9    sync::{Arc, Mutex, RwLock},
10};
11
12use custom_debug_derive::Debug;
13use futures::{
14    future::{Future, TryFutureExt as _},
15    stream::{self, AbortHandle, FuturesOrdered, FuturesUnordered, StreamExt},
16};
17#[cfg(with_metrics)]
18use linera_base::prometheus_util::MeasureLatency as _;
19use linera_base::{
20    crypto::{CryptoHash, Signer as _, ValidatorPublicKey},
21    data_types::{
22        ApplicationDescription, ArithmeticError, Blob, BlockHeight, ChainDescription, Epoch, Round,
23        TimeDelta, Timestamp,
24    },
25    ensure,
26    hashed::Hashed,
27    identifiers::{AccountOwner, ApplicationId, BlobId, BlobType, ChainId, EventId, StreamId},
28    time::Duration,
29};
30#[cfg(not(target_arch = "wasm32"))]
31use linera_base::{data_types::Bytecode, identifiers::ModuleId, vm::VmRuntime};
32use linera_chain::{
33    data_types::{
34        BlockExecutionOutcome, BlockProposal, BundleExecutionPolicy, ChainAndHeight, LiteVote,
35        OriginalProposal, ProposedBlock,
36    },
37    justification::JustificationChain,
38    manager::LockingBlock,
39    types::{
40        Block, CertificateValue, Certified, ConfirmedBlock, ConfirmedBlockCertificate,
41        GenericCertificate, LiteCertificate, Timeout, ValidatedBlock, ValidatedBlockCertificate,
42    },
43    ChainError, ChainIdSet,
44};
45use linera_execution::{committee::Committee, ExecutionError};
46use linera_storage::{Arc as CacheArc, Clock as _, ResultReadCertificates, Storage as _};
47use rand::seq::SliceRandom;
48use received_log::ReceivedLogs;
49use serde::{Deserialize, Serialize};
50use tokio::sync::mpsc;
51use tracing::{debug, error, info, instrument, trace, warn};
52
53use crate::{
54    data_types::{ChainInfo, ChainInfoQuery, ChainInfoResponse},
55    environment::Environment,
56    local_node::{LocalNodeClient, LocalNodeError},
57    node::{CrossChainMessageDelivery, NodeError, ValidatorNode as _, ValidatorNodeProvider as _},
58    notifier::{ChannelNotifier, Notifier as _},
59    remote_node::RemoteNode,
60    updater::{communicate_with_quorum, CommunicateAction, RemoteNodeUpdater},
61    worker::{Notification, ProcessableCertificate, Reason, WorkerError, WorkerState},
62    ChainWorkerConfig, ProcessConfirmedBlockMode, CHAIN_INFO_MAX_RECEIVED_LOG_ENTRIES,
63};
64
65/// The client for interacting with a single chain.
66pub mod chain_client;
67pub use chain_client::ChainClient;
68
69pub use crate::data_types::ClientOutcome;
70
71#[cfg(test)]
72#[path = "../unit_tests/client_tests.rs"]
73mod client_tests;
74pub mod requests_scheduler;
75
76pub use requests_scheduler::{RequestsScheduler, RequestsSchedulerConfig, ScoringWeights};
77mod received_log;
78mod validator_trackers;
79
80#[cfg(with_metrics)]
81pub(crate) mod metrics {
82    use linera_base::prometheus_util::{
83        exponential_bucket_interval, exponential_bucket_latencies, register_histogram_vec,
84        register_int_counter, register_int_counter_vec,
85    };
86    use prometheus::{HistogramVec, IntCounter, IntCounterVec};
87
88    linera_base::declare_metrics! {
89        pub static PROCESS_INBOX_WITHOUT_PREPARE_LATENCY: HistogramVec =
90            register_histogram_vec(
91                "process_inbox_latency",
92                "process_inbox latency",
93                &[],
94                exponential_bucket_latencies(60_000.0),
95            );
96
97        pub static PREPARE_CHAIN_LATENCY: HistogramVec =
98            register_histogram_vec(
99                "prepare_chain_latency",
100                "prepare_chain latency",
101                &[],
102                exponential_bucket_latencies(60_000.0),
103            );
104
105        pub static SYNCHRONIZE_CHAIN_STATE_LATENCY: HistogramVec =
106            register_histogram_vec(
107                "synchronize_chain_state_latency",
108                "synchronize_chain_state latency",
109                &[],
110                exponential_bucket_latencies(600_000.0),
111            );
112
113        pub static EXECUTE_BLOCK_LATENCY: HistogramVec =
114            register_histogram_vec(
115                "execute_block_latency",
116                "execute_block latency",
117                &[],
118                exponential_bucket_latencies(10_000.0),
119            );
120
121        pub static FIND_RECEIVED_CERTIFICATES_LATENCY: HistogramVec =
122            register_histogram_vec(
123                "find_received_certificates_latency",
124                "find_received_certificates latency",
125                &[],
126                exponential_bucket_latencies(3_600_000.0),
127            );
128
129        pub static FIND_RECEIVED_CERTIFICATES_LOG_ENTRIES: HistogramVec =
130            register_histogram_vec(
131                "find_received_certificates_log_entries",
132                "Number of received-log entries collected from the validators, per call",
133                &[],
134                exponential_bucket_interval(1.0, 1_000_000.0),
135            );
136
137        pub static FIND_RECEIVED_CERTIFICATES_SENDER_CHAINS: HistogramVec =
138            register_histogram_vec(
139                "find_received_certificates_sender_chains",
140                "Number of distinct sender chains to synchronize, per call",
141                &[],
142                exponential_bucket_interval(1.0, 100_000.0),
143            );
144
145        pub static SENDER_CERTIFICATES_DISCOVERED_TOTAL: IntCounter =
146            register_int_counter(
147                "sender_certificates_discovered_total",
148                "Total number of sender certificates advertised by the validators' received logs",
149            );
150
151        pub static SENDER_CERTIFICATES_MISSING_TOTAL: IntCounter =
152            register_int_counter(
153                "sender_certificates_missing_total",
154                "Total number of sender certificates not already known locally, hence downloaded",
155            );
156
157        pub static BLOCK_STAGING_FAILURES_TOTAL: IntCounterVec =
158            register_int_counter_vec(
159                "block_staging_failures_total",
160                "Total number of client block staging (execute_block) failures, labelled by error type",
161                &["error_type"],
162            );
163    }
164}
165
166/// Default number of certificates to download in a single batch.
167pub static DEFAULT_CERTIFICATE_DOWNLOAD_BATCH_SIZE: u64 = 500;
168/// Default number of certificates to upload in a single batch.
169pub static DEFAULT_CERTIFICATE_UPLOAD_BATCH_SIZE: usize = 500;
170/// Default number of sender-chain certificates to download in a single batch.
171pub static DEFAULT_SENDER_CERTIFICATE_DOWNLOAD_BATCH_SIZE: usize = 20_000;
172/// Default maximum number of concurrent event stream queries.
173pub static DEFAULT_MAX_EVENT_STREAM_QUERIES: usize = 1000;
174/// Default maximum number of certificate batch downloads to run concurrently.
175pub static DEFAULT_MAX_CONCURRENT_BATCH_DOWNLOADS: usize = 1;
176
177/// Identifies which operation a timing measurement refers to.
178#[derive(Debug, Clone, Copy)]
179#[allow(missing_docs)]
180pub enum TimingType {
181    ExecuteOperations,
182    ExecuteBlock,
183    SubmitBlockProposal,
184    UpdateValidators,
185}
186
187/// Defines what type of notifications we should process for a chain:
188/// - do we fully participate in consensus and download sender chains?
189/// - or do we only follow the chain's blocks without participating?
190/// - or do we only care about blocks containing events from some particular streams?
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum ListeningMode {
193    /// Listen to everything: all blocks for the chain and all blocks from sender chains,
194    /// and participate in rounds.
195    FullChain,
196    /// Listen to all blocks for the chain, but don't download sender chain blocks or participate
197    /// in rounds. Use this when interested in the chain's state but not intending to propose
198    /// blocks (e.g., because we're not a chain owner).
199    FollowChain,
200    /// Only listen to blocks which contain events from those streams.
201    EventsOnly(BTreeSet<StreamId>),
202}
203
204impl PartialOrd for ListeningMode {
205    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
206        match (self, other) {
207            (ListeningMode::FullChain, ListeningMode::FullChain) => Some(Ordering::Equal),
208            (ListeningMode::FullChain, _) => Some(Ordering::Greater),
209            (_, ListeningMode::FullChain) => Some(Ordering::Less),
210            (ListeningMode::FollowChain, ListeningMode::FollowChain) => Some(Ordering::Equal),
211            (ListeningMode::FollowChain, ListeningMode::EventsOnly(_)) => Some(Ordering::Greater),
212            (ListeningMode::EventsOnly(_), ListeningMode::FollowChain) => Some(Ordering::Less),
213            (ListeningMode::EventsOnly(a), ListeningMode::EventsOnly(b)) => {
214                if a == b {
215                    Some(Ordering::Equal)
216                } else if a.is_superset(b) {
217                    Some(Ordering::Greater)
218                } else if b.is_superset(a) {
219                    Some(Ordering::Less)
220                } else {
221                    None
222                }
223            }
224        }
225    }
226}
227
228impl ListeningMode {
229    /// Returns whether a notification with this reason should be processed under this listening
230    /// mode.
231    pub fn is_relevant(&self, reason: &Reason) -> bool {
232        match (reason, self) {
233            // NewEvents is only processed in EventsOnly mode, the other modes depend on
234            // the NewBlock notification.
235            (Reason::NewEvents { .. }, ListeningMode::FollowChain | ListeningMode::FullChain) => {
236                false
237            }
238            // FullChain processes everything.
239            (_, ListeningMode::FullChain) => true,
240            // FollowChain processes new blocks on the chain itself, including blocks that
241            // produced events.
242            (Reason::NewBlock { .. }, ListeningMode::FollowChain) => true,
243            (_, ListeningMode::FollowChain) => false,
244            // EventsOnly only processes events from relevant streams.
245            (Reason::NewEvents { event_streams, .. }, ListeningMode::EventsOnly(relevant)) => {
246                relevant.intersection(event_streams).next().is_some()
247            }
248            (_, ListeningMode::EventsOnly(_)) => false,
249        }
250    }
251
252    /// Widens this mode to also cover the given mode, keeping the more inclusive of the two.
253    pub fn extend(&mut self, other: Option<ListeningMode>) {
254        match (self, other) {
255            (_, None) => (),
256            (ListeningMode::FullChain, _) => (),
257            (mode, Some(ListeningMode::FullChain)) => {
258                *mode = ListeningMode::FullChain;
259            }
260            (ListeningMode::FollowChain, _) => (),
261            (mode, Some(ListeningMode::FollowChain)) => {
262                *mode = ListeningMode::FollowChain;
263            }
264            (
265                ListeningMode::EventsOnly(self_events),
266                Some(ListeningMode::EventsOnly(other_events)),
267            ) => {
268                self_events.extend(other_events);
269            }
270        }
271    }
272
273    /// Returns whether this mode implies follow-only behavior (i.e., not participating in
274    /// consensus rounds).
275    pub fn is_follow_only(&self) -> bool {
276        !matches!(self, ListeningMode::FullChain)
277    }
278
279    /// Returns whether this is a full chain mode (synchronizing sender chains and updating
280    /// inboxes).
281    pub fn is_full(&self) -> bool {
282        matches!(self, ListeningMode::FullChain)
283    }
284
285    /// Returns whether this mode requires synchronizing the chain's own state.
286    pub fn should_sync_chain_state(&self) -> bool {
287        match self {
288            ListeningMode::FullChain | ListeningMode::FollowChain => true,
289            ListeningMode::EventsOnly(_) => false,
290        }
291    }
292}
293
294/// The per-chain [`ListeningMode`]s tracked by a local node, together with a memoized
295/// [`Hashed`] of the fully-tracked subset.
296///
297/// The hash is the version of the outbox index (see
298/// [`ChainStateView::outbox_index_tracked_hash`]). Caching it here — recomputed only when a chain
299/// newly becomes `FullChain`, which is rare and client-side — lets every cross-chain operation
300/// compare and reconcile the index in `O(1)` instead of rehashing the tracked set each time.
301///
302/// [`ChainStateView::outbox_index_tracked_hash`]: linera_chain::ChainStateView
303#[derive(Debug)]
304pub struct ChainModes {
305    modes: BTreeMap<ChainId, ListeningMode>,
306    full: Arc<Hashed<ChainIdSet>>,
307}
308
309impl Default for ChainModes {
310    fn default() -> Self {
311        Self::new(BTreeMap::new())
312    }
313}
314
315impl ChainModes {
316    /// Builds the listening modes from `modes`, computing the tracked-set hash once.
317    pub fn new(modes: BTreeMap<ChainId, ListeningMode>) -> Self {
318        let full = Self::compute_full(&modes);
319        Self { modes, full }
320    }
321
322    fn compute_full(modes: &BTreeMap<ChainId, ListeningMode>) -> Arc<Hashed<ChainIdSet>> {
323        Arc::new(Hashed::new(ChainIdSet(
324            modes
325                .iter()
326                .filter(|(_, mode)| mode.is_full())
327                .map(|(id, _)| *id)
328                .collect(),
329        )))
330    }
331
332    /// The fully-tracked chains together with their memoized hash (`O(1)` clone).
333    pub fn full(&self) -> Arc<Hashed<ChainIdSet>> {
334        self.full.clone()
335    }
336
337    /// Returns the listening mode for `chain_id`, if it is tracked.
338    pub fn get(&self, chain_id: &ChainId) -> Option<&ListeningMode> {
339        self.modes.get(chain_id)
340    }
341
342    /// Merges `mode` into the entry for `chain_id` — monotonic in the listening-mode order, so it
343    /// never weakens an existing entry — and returns the resulting mode. The tracked-set hash is
344    /// recomputed only if the chain newly became `FullChain`.
345    pub fn extend_mode(&mut self, chain_id: ChainId, mode: ListeningMode) -> ListeningMode {
346        let entry = self
347            .modes
348            .entry(chain_id)
349            .or_insert_with(|| ListeningMode::EventsOnly(BTreeSet::new()));
350        let was_full = entry.is_full();
351        entry.extend(Some(mode));
352        let result = entry.clone();
353        if !was_full && result.is_full() {
354            self.full = Self::compute_full(&self.modes);
355        }
356        result
357    }
358
359    /// Stops tracking `chain_id`, removing its entry entirely. Returns the removed mode, if any.
360    /// The tracked-set hash is recomputed only if the removed chain was `FullChain`.
361    pub fn remove_mode(&mut self, chain_id: &ChainId) -> Option<ListeningMode> {
362        let removed = self.modes.remove(chain_id)?;
363        if removed.is_full() {
364            self.full = Self::compute_full(&self.modes);
365        }
366        Some(removed)
367    }
368}
369
370/// A builder that creates [`ChainClient`]s which share the cache and notifiers.
371pub struct Client<Env: Environment> {
372    environment: Env,
373    /// Local node to manage the execution state and the local storage of the chains that we are
374    /// tracking.
375    pub local_node: LocalNodeClient<Env::Storage>,
376    /// Manages the requests sent to validator nodes.
377    requests_scheduler: Arc<RequestsScheduler<Env>>,
378    /// The admin chain ID.
379    admin_chain_id: ChainId,
380    /// Chains that should be tracked by the client, along with their listening mode.
381    /// The presence of a chain in this map means it is tracked by the local node.
382    chain_modes: Arc<RwLock<ChainModes>>,
383    /// References to clients waiting for chain notifications.
384    notifier: Arc<ChannelNotifier<Notification>>,
385    /// Chain state for the managed chains.
386    chains: papaya::HashMap<ChainId, chain_client::State>,
387    /// Configuration options.
388    options: chain_client::Options,
389}
390
391/// Boxed future returned by `receive_sender_certificate`. It is `Send` off the `web`
392/// target (where futures must be `Send`) and `?Send` on `web` (single-threaded, where
393/// the validator node is not `Sync`).
394#[cfg(not(web))]
395type ReceiveSenderCertificateFuture<'a> =
396    std::pin::Pin<Box<dyn Future<Output = Result<(), chain_client::Error>> + Send + 'a>>;
397#[cfg(web)]
398type ReceiveSenderCertificateFuture<'a> =
399    std::pin::Pin<Box<dyn Future<Output = Result<(), chain_client::Error>> + 'a>>;
400
401impl<Env: Environment> Client<Env> {
402    /// Creates a new `Client` with a new cache and notifiers.
403    #[instrument(level = "trace", skip_all)]
404    #[expect(clippy::too_many_arguments)]
405    pub fn new(
406        environment: Env,
407        admin_chain_id: ChainId,
408        long_lived_services: bool,
409        chain_modes: impl IntoIterator<Item = (ChainId, ListeningMode)>,
410        name: impl Into<String>,
411        chain_worker_ttl: Option<Duration>,
412        sender_chain_worker_ttl: Option<Duration>,
413        cross_chain_batch_size_limit: usize,
414        options: chain_client::Options,
415        block_cache_size: usize,
416        execution_state_cache_size: usize,
417        requests_scheduler_config: &requests_scheduler::RequestsSchedulerConfig,
418    ) -> Self {
419        let mut modes = chain_modes.into_iter().collect::<BTreeMap<_, _>>();
420        // The client needs the admin chain fully synced for epoch tracking, so
421        // promote it (or insert) into `FullChain`. `extend` is monotonic in the
422        // listening mode order, so this never weakens an existing entry.
423        modes
424            .entry(admin_chain_id)
425            .or_insert(ListeningMode::FullChain)
426            .extend(Some(ListeningMode::FullChain));
427        let chain_modes = Arc::new(RwLock::new(ChainModes::new(modes)));
428        let config = ChainWorkerConfig {
429            nickname: name.into(),
430            long_lived_services,
431            allow_inactive_chains: true,
432            ttl: chain_worker_ttl,
433            sender_chain_ttl: sender_chain_worker_ttl,
434            block_cache_size,
435            execution_state_cache_size,
436            cross_chain_batch_size_limit,
437            ..ChainWorkerConfig::default()
438        };
439        let state = WorkerState::new(
440            environment.storage().clone(),
441            config,
442            Some(chain_modes.clone()),
443        );
444        let clock = environment.storage().clock().clone();
445        let local_node = LocalNodeClient::new(state);
446        let requests_scheduler = Arc::new(RequestsScheduler::new(
447            vec![],
448            requests_scheduler_config,
449            clock,
450        ));
451
452        Self {
453            environment,
454            local_node,
455            requests_scheduler,
456            chains: papaya::HashMap::new(),
457            admin_chain_id,
458            chain_modes,
459            notifier: Arc::new(ChannelNotifier::default()),
460            options,
461        }
462    }
463
464    /// Returns the chain ID of the admin chain.
465    pub fn admin_chain_id(&self) -> ChainId {
466        self.admin_chain_id
467    }
468
469    /// Subscribes to notifications for the given chain IDs.
470    pub fn subscribe(
471        &self,
472        chain_ids: Vec<ChainId>,
473    ) -> tokio::sync::mpsc::UnboundedReceiver<Notification> {
474        self.notifier.subscribe(chain_ids)
475    }
476
477    /// Adds additional chain IDs to an existing subscription.
478    pub fn subscribe_extra(
479        &self,
480        chain_ids: Vec<ChainId>,
481        sender: &tokio::sync::mpsc::UnboundedSender<Notification>,
482    ) {
483        self.notifier.add_sender(chain_ids, sender);
484    }
485
486    /// Returns the storage client used by this client's local node.
487    pub fn storage_client(&self) -> &Env::Storage {
488        self.environment.storage()
489    }
490
491    /// Tries to read a certificate from local storage, using the hash if available
492    /// (fast path) or falling back to a height-based lookup.
493    async fn try_read_local_certificate(
494        &self,
495        chain_id: ChainId,
496        height: BlockHeight,
497        hash: Option<CryptoHash>,
498    ) -> Result<Option<CacheArc<ConfirmedBlockCertificate>>, chain_client::Error> {
499        if let Some(hash) = hash {
500            return Ok(self.storage_client().read_certificate(hash).await?);
501        }
502        let results = self
503            .storage_client()
504            .read_certificates_by_heights(chain_id, &[height])
505            .await?;
506        Ok(results.into_iter().next().flatten())
507    }
508
509    /// Returns the provider used to connect to validator nodes.
510    pub fn validator_node_provider(&self) -> &Env::Network {
511        self.environment.network()
512    }
513
514    /// Handles any pending local cross-chain requests, notifying subscribers.
515    pub async fn retry_pending_cross_chain_requests(
516        &self,
517        sender_chain: ChainId,
518    ) -> Result<(), LocalNodeError> {
519        self.local_node
520            .retry_pending_cross_chain_requests(sender_chain, &self.notifier)
521            .await
522    }
523
524    /// Returns a reference to the client's [`Signer`][crate::environment::Signer].
525    #[instrument(level = "trace", skip(self))]
526    pub fn signer(&self) -> &Env::Signer {
527        self.environment.signer()
528    }
529
530    /// Returns whether the signer has a key for the given owner.
531    pub async fn has_key_for(&self, owner: &AccountOwner) -> Result<bool, chain_client::Error> {
532        self.signer()
533            .contains_key(owner)
534            .await
535            .map_err(chain_client::Error::signer_failure)
536    }
537
538    /// Returns a reference to the client's [`Wallet`][crate::environment::Wallet].
539    pub fn wallet(&self) -> &Env::Wallet {
540        self.environment.wallet()
541    }
542
543    /// Extends the listening mode for a chain, combining with the existing mode if present.
544    /// Returns the resulting mode.
545    #[instrument(level = "trace", skip(self))]
546    pub fn extend_chain_mode(&self, chain_id: ChainId, mode: ListeningMode) -> ListeningMode {
547        self.chain_modes
548            .write()
549            .expect("Panics should not happen while holding a lock to `chain_modes`")
550            .extend_mode(chain_id, mode)
551    }
552
553    /// Stops tracking a chain, removing its listening mode. Returns the removed mode, if any.
554    #[instrument(level = "trace", skip(self))]
555    pub fn remove_chain_mode(&self, chain_id: ChainId) -> Option<ListeningMode> {
556        self.chain_modes
557            .write()
558            .expect("Panics should not happen while holding a lock to `chain_modes`")
559            .remove_mode(&chain_id)
560    }
561
562    /// Returns the listening mode for a chain, if it is tracked.
563    pub fn chain_mode(&self, chain_id: ChainId) -> Option<ListeningMode> {
564        self.chain_modes
565            .read()
566            .expect("Panics should not happen while holding a lock to `chain_modes`")
567            .get(&chain_id)
568            .cloned()
569    }
570
571    /// Returns whether a chain is fully tracked by the local node.
572    pub fn is_tracked(&self, chain_id: ChainId) -> bool {
573        self.chain_modes
574            .read()
575            .expect("Panics should not happen while holding a lock to `chain_modes`")
576            .get(&chain_id)
577            .is_some_and(ListeningMode::is_full)
578    }
579
580    /// Creates a new `ChainClient`.
581    #[expect(clippy::too_many_arguments)]
582    #[instrument(level = "trace", skip_all, fields(chain_id, next_block_height))]
583    pub fn create_chain_client(
584        self: &Arc<Self>,
585        chain_id: ChainId,
586        block_hash: Option<CryptoHash>,
587        next_block_height: BlockHeight,
588        pending_proposal: &Option<PendingProposal>,
589        preferred_owner: Option<AccountOwner>,
590        timing_sender: Option<mpsc::UnboundedSender<(u64, TimingType)>>,
591        follow_only: bool,
592    ) -> ChainClient<Env> {
593        // If the entry already exists we assume that the entry is more up to date than
594        // the arguments: If they were read from the wallet file, they might be stale.
595        self.chains.pin().get_or_insert_with(chain_id, || {
596            chain_client::State::new(pending_proposal.clone(), follow_only)
597        });
598
599        ChainClient::new(
600            self.clone(),
601            chain_id,
602            self.options.clone(),
603            block_hash,
604            next_block_height,
605            preferred_owner,
606            timing_sender,
607        )
608    }
609
610    /// Returns whether the given chain is in follow-only mode.
611    fn is_chain_follow_only(&self, chain_id: ChainId) -> bool {
612        self.chains
613            .pin()
614            .get(&chain_id)
615            .is_some_and(|state| state.is_follow_only())
616    }
617
618    /// Sets whether the given chain is in follow-only mode.
619    pub fn set_chain_follow_only(&self, chain_id: ChainId, follow_only: bool) {
620        self.chains
621            .pin()
622            .update(chain_id, |state| state.with_follow_only(follow_only));
623    }
624
625    /// Fetches the chain description blob if needed, and returns the chain info.
626    async fn fetch_chain_info(
627        &self,
628        chain_id: ChainId,
629        validators: &[RemoteNode<Env::ValidatorNode>],
630    ) -> Result<Box<ChainInfo>, chain_client::Error> {
631        match self.local_node.chain_info(chain_id).await {
632            Ok(info) => Ok(info),
633            Err(LocalNodeError::BlobsNotFound(blob_ids)) => {
634                // Make sure the admin chain is up to date so we can validate the
635                // certificate that creates this chain's description blob.
636                self.synchronize_chain_state(self.admin_chain_id).await?;
637                self.update_local_node_with_blobs_from(blob_ids, validators)
638                    .await?;
639                Ok(self.local_node.chain_info(chain_id).await?)
640            }
641            Err(err) => Err(err.into()),
642        }
643    }
644
645    /// Downloads and processes all certificates up to (excluding) the specified height.
646    #[instrument(level = "trace", skip(self))]
647    async fn download_certificates(
648        &self,
649        chain_id: ChainId,
650        target_next_block_height: BlockHeight,
651    ) -> Result<Box<ChainInfo>, chain_client::Error> {
652        let validators = self.validator_nodes().await?;
653        let mut info = Box::pin(self.fetch_chain_info(chain_id, &validators)).await?;
654        if target_next_block_height <= info.next_block_height {
655            return Ok(info);
656        }
657        info = self
658            .load_local_certificates(chain_id, target_next_block_height, None)
659            .await?;
660        let mut next_height = info.next_block_height;
661        // Download remaining batches using all validators with staggered fallback.
662        while next_height < target_next_block_height {
663            let limit = u64::from(target_next_block_height)
664                .checked_sub(u64::from(next_height))
665                .ok_or(ArithmeticError::Overflow)?
666                .min(self.options.certificate_download_batch_size);
667            let certificates = self
668                .requests_scheduler
669                .download_certificates_from_validators(
670                    &validators,
671                    chain_id,
672                    next_height,
673                    limit,
674                    self.options.certificate_batch_download_hedge_delay,
675                )
676                .await?;
677            let Some(new_info) = self
678                .process_certificates(
679                    &validators,
680                    certificates,
681                    None,
682                    ProcessConfirmedBlockMode::Execute,
683                )
684                .await?
685            else {
686                break;
687            };
688            assert!(new_info.next_block_height > next_height);
689            next_height = new_info.next_block_height;
690            info = new_info;
691        }
692        ensure!(
693            target_next_block_height <= info.next_block_height,
694            chain_client::Error::CannotDownloadCertificates {
695                chain_id,
696                target_next_block_height,
697            }
698        );
699        Ok(info)
700    }
701
702    /// Loads and processes certificates from local storage for the given chain, from the
703    /// current local height up to `end`. Returns the chain info after processing.
704    /// If `until_block_time` is `Some`, stops before processing any certificate whose
705    /// block timestamp is >= the given value (exclusive).
706    async fn load_local_certificates(
707        &self,
708        chain_id: ChainId,
709        end: BlockHeight,
710        until_block_time: Option<Timestamp>,
711    ) -> Result<Box<ChainInfo>, chain_client::Error> {
712        let mut last_info = self.local_node.chain_info(chain_id).await?;
713        let next_height = last_info.next_block_height;
714        let hashes = self
715            .local_node
716            .get_preprocessed_block_hashes(chain_id, next_height, end)
717            .await?;
718        let certificates = self.storage_client().read_certificates(&hashes).await?;
719        let certificates = match ResultReadCertificates::new(certificates, hashes) {
720            ResultReadCertificates::Certificates(certificates) => certificates,
721            ResultReadCertificates::InvalidHashes(hashes) => {
722                return Err(chain_client::Error::ReadCertificatesError(hashes))
723            }
724        };
725        for certificate in certificates {
726            if let Some(until) = until_block_time {
727                if certificate.value().block().header.timestamp >= until {
728                    break;
729                }
730            }
731            last_info = self
732                .handle_certificate::<ConfirmedBlock>(certificate)
733                .await?
734                .info;
735        }
736        Ok(last_info)
737    }
738
739    /// Downloads and processes certificates from the given validator.
740    ///
741    /// Stops when either condition is met:
742    /// - `stop`: the local chain has reached that height (exclusive).
743    /// - `until_block_time`: the next block's timestamp is >= that value (exclusive).
744    #[instrument(level = "trace", skip_all)]
745    async fn download_certificates_from(
746        &self,
747        remote_node: &RemoteNode<Env::ValidatorNode>,
748        chain_id: ChainId,
749        stop: BlockHeight,
750        until_block_time: Option<Timestamp>,
751    ) -> Result<Box<ChainInfo>, chain_client::Error> {
752        let mut last_info = self
753            .load_local_certificates(chain_id, stop, until_block_time)
754            .await?;
755        let mut next_height = last_info.next_block_height;
756
757        if next_height >= stop {
758            return Ok(last_info);
759        }
760
761        // Download remaining certificates from the remote node using a pipelined
762        // sliding window. A background task downloads up to `max_concurrent_batch_downloads`
763        // batches concurrently and sends them through a channel for sequential processing.
764        #[cfg(not(web))]
765        type CertificateBatchFuture = std::pin::Pin<
766            Box<dyn Future<Output = Result<Vec<ConfirmedBlockCertificate>, NodeError>> + Send>,
767        >;
768        #[cfg(web)]
769        type CertificateBatchFuture = std::pin::Pin<
770            Box<dyn Future<Output = Result<Vec<ConfirmedBlockCertificate>, NodeError>>>,
771        >;
772
773        let max_concurrent = self.options.max_concurrent_batch_downloads;
774        let batch_size = self.options.certificate_download_batch_size;
775        let (sender, mut receiver) = tokio::sync::mpsc::channel(max_concurrent);
776        let scheduler = self.requests_scheduler.clone();
777        let remote = remote_node.clone();
778
779        let download_task = linera_base::Task::spawn(async move {
780            let mut download_height = next_height;
781            let mut in_flight = FuturesOrdered::<CertificateBatchFuture>::new();
782
783            let try_enqueue = |in_flight: &mut FuturesOrdered<CertificateBatchFuture>,
784                               download_height: &mut BlockHeight| {
785                if *download_height >= stop {
786                    return;
787                }
788                let limit = u64::from(stop)
789                    .saturating_sub(u64::from(*download_height))
790                    .min(batch_size);
791                let height = *download_height;
792                let scheduler = scheduler.clone();
793                let remote = remote.clone();
794                in_flight.push_back(Box::pin(async move {
795                    scheduler
796                        .download_certificates(&remote, chain_id, height, limit)
797                        .await
798                }));
799                *download_height = BlockHeight(u64::from(*download_height) + limit);
800            };
801
802            while in_flight.len() < max_concurrent && download_height < stop {
803                try_enqueue(&mut in_flight, &mut download_height);
804            }
805
806            while let Some(result) = in_flight.next().await {
807                if sender.send(result).await.is_err() {
808                    break;
809                }
810                try_enqueue(&mut in_flight, &mut download_height);
811            }
812        });
813
814        // Process downloaded batches sequentially.
815        while let Some(result) = receiver.recv().await {
816            let certificates = result?;
817            let Some(info) = self
818                .process_certificates(
819                    slice::from_ref(remote_node),
820                    certificates,
821                    until_block_time,
822                    ProcessConfirmedBlockMode::Execute,
823                )
824                .await?
825            else {
826                break;
827            };
828            assert!(info.next_block_height >= next_height);
829            next_height = info.next_block_height;
830            last_info = info;
831        }
832        // Await the downloader so any panic inside the spawned task surfaces here
833        // instead of being silently swallowed when the channel closes.
834        download_task.await;
835        Ok(last_info)
836    }
837
838    async fn download_blobs(
839        &self,
840        remote_nodes: &[RemoteNode<Env::ValidatorNode>],
841        blob_ids: &[BlobId],
842    ) -> Result<(), chain_client::Error> {
843        let blobs = &self
844            .requests_scheduler
845            .download_blobs(
846                remote_nodes,
847                blob_ids,
848                self.options.blob_download_hedge_delay,
849            )
850            .await?
851            .ok_or_else(|| {
852                chain_client::Error::RemoteNodeError(NodeError::BlobsNotFound(blob_ids.to_vec()))
853            })?;
854        self.local_node.store_blobs(blobs).await.map_err(Into::into)
855    }
856
857    /// Downloads the publisher chain certificates that contain the given events,
858    /// using the event block height index on validators. Queries a validator for
859    /// the block heights, downloads those certificates, and processes them — all
860    /// as one atomic unit per validator attempt, with staggered fallback.
861    #[instrument(level = "trace", skip_all)]
862    pub(crate) async fn download_certificates_for_events(
863        &self,
864        event_ids: &[EventId],
865    ) -> Result<(), chain_client::Error> {
866        let mut validators = self.validator_nodes().await?;
867        let hedge_delay = self.options.certificate_batch_download_hedge_delay;
868        let mut remaining_event_ids = event_ids.to_vec();
869
870        while !remaining_event_ids.is_empty() {
871            let remaining_ref = &remaining_event_ids;
872            validators.shuffle(&mut rand::thread_rng());
873            let result = communicate_concurrently(
874                &validators,
875                move |remote_node| {
876                    let validator_key = remote_node.public_key;
877                    let validator_address = remote_node.address();
878                    Box::pin(async move {
879                        // Query this validator for the block heights.
880                        let heights = remote_node
881                            .node
882                            .event_block_heights(remaining_ref.to_vec())
883                            .await?;
884
885                        // Separate resolved and unresolved events.
886                        let mut chain_heights = BTreeMap::<_, BTreeSet<_>>::new();
887                        let mut expected_events = BTreeMap::<_, HashSet<EventId>>::new();
888                        let mut unresolved = Vec::new();
889                        for (event_id, maybe_height) in remaining_ref.iter().zip(heights) {
890                            if let Some(height) = maybe_height {
891                                chain_heights
892                                    .entry(event_id.chain_id)
893                                    .or_default()
894                                    .insert(height);
895                                expected_events
896                                    .entry((event_id.chain_id, height))
897                                    .or_default()
898                                    .insert(event_id.clone());
899                            } else {
900                                unresolved.push(event_id.clone());
901                            }
902                        }
903                        if chain_heights.is_empty() {
904                            // This validator has no useful information.
905                            return Err(chain_client::Error::from(NodeError::EventsNotFound(remaining_ref.clone())));
906                        }
907
908                        // Download certificates and verify them.
909                        let mut checked_certificates = Vec::<ConfirmedBlockCertificate>::new();
910                        for (chain_id, heights) in chain_heights {
911                            let heights_vec = heights.into_iter().collect::<Vec<_>>();
912                            let certificates = self
913                                .requests_scheduler
914                                .download_certificates_by_heights(
915                                    &remote_node,
916                                    chain_id,
917                                    heights_vec,
918                                )
919                                .await?;
920                            for cert in &certificates {
921                                // Verify the block contains the expected events.
922                                let block = cert.block();
923                                let block_event_ids = block.event_ids().collect::<HashSet<_>>();
924                                if let Some(expected_event_ids) =
925                                    expected_events.get(&(chain_id, block.header.height))
926                                {
927                                    if !expected_event_ids.is_subset(&block_event_ids) {
928                                        tracing::debug!(
929                                            %validator_address, ?expected_event_ids, ?block_event_ids,
930                                            "validator lied about events in block."
931                                        );
932                                        return Err(NodeError::UnexpectedCertificateValue.into());
933                                    }
934                                }
935                            }
936                            for cert in certificates {
937                                self.check_certificate(&cert)
938                                    .await
939                                    .map_err(|error| {
940                                        tracing::debug!(
941                                            %validator_address, %error,
942                                            "invalid certificate"
943                                        );
944                                        error
945                                    })?
946                                    .into_result()
947                                    .map_err(|error| {
948                                        tracing::debug!(
949                                            %validator_address, %error,
950                                            "could not check certificate"
951                                        );
952                                        error
953                                    })?;
954                                checked_certificates.push(cert);
955                            }
956                        }
957                        Ok((checked_certificates, unresolved, validator_key))
958                    })
959                },
960                hedge_delay,
961                self.storage_client().clock(),
962            )
963            .await;
964
965            match result {
966                Ok((certificates, unresolved, validator_key)) => {
967                    for certificate in certificates {
968                        let mode = ReceiveCertificateMode::AlreadyChecked;
969                        self.receive_sender_certificate(
970                            self.storage_client().cache_certificate(certificate),
971                            mode,
972                            None,
973                        )
974                        .await?;
975                    }
976                    validators.retain(|node| node.public_key != validator_key);
977                    remaining_event_ids = unresolved;
978                }
979                Err(errors) => {
980                    for (validator, error) in &errors {
981                        warn!(
982                            %validator,
983                            %error,
984                            "failed to download event certificates from validator",
985                        );
986                    }
987                    // All validators failed; no point retrying.
988                    return Err(NodeError::EventsNotFound(remaining_event_ids).into());
989                }
990            }
991        }
992        Ok(())
993    }
994
995    /// Downloads the checkpoint certificate at `checkpoint_height` from `remote_node`
996    /// and processes it locally, if our chain isn't already past that height. The
997    /// worker's `process_confirmed_block` recognises the gap-plus-checkpoint case and
998    /// installs the chain's execution state from the checkpoint blob before re-running
999    /// the certificate.
1000    ///
1001    /// The certificate's signatures are still verified against the committee resolved
1002    /// from the admin chain's epoch event stream, so the remote node is trusted only
1003    /// to point us at a height — not to forge the snapshot itself.
1004    #[instrument(level = "trace", skip_all)]
1005    async fn bootstrap_chain_from_checkpoint(
1006        &self,
1007        remote_node: &RemoteNode<Env::ValidatorNode>,
1008        chain_id: ChainId,
1009        checkpoint_height: BlockHeight,
1010    ) -> Result<(), chain_client::Error> {
1011        let local_next = match self.local_node.chain_info(chain_id).await {
1012            Ok(info) => info.next_block_height,
1013            // A freshly-created follower whose storage doesn't yet hold this
1014            // chain's description blob: treat as height 0 and let the
1015            // checkpoint cert install the snapshot. The chain description is
1016            // the only blob `chain_info` ever needs.
1017            Err(LocalNodeError::BlobsNotFound(_)) => BlockHeight::ZERO,
1018            Err(err) => return Err(err.into()),
1019        };
1020        if local_next > checkpoint_height {
1021            return Ok(());
1022        }
1023        let certificates = remote_node
1024            .download_certificates_by_heights(chain_id, vec![checkpoint_height])
1025            .await?;
1026        if certificates.is_empty() {
1027            // The validator advertised a checkpoint height it can't actually serve;
1028            // skip and let the regular sync path take over.
1029            return Ok(());
1030        }
1031        // The first attempt at processing the checkpoint cert will fall into the
1032        // worker's `BlocksNotFound` pre-check if pre-checkpoint sender blocks are
1033        // missing; `handle_certificate_with_retry` downloads them by hash and
1034        // retries, so by the time `process_certificates` returns the chain has
1035        // both its restored state and every certified sender block in storage.
1036        self.process_certificates(
1037            slice::from_ref(remote_node),
1038            certificates,
1039            None,
1040            ProcessConfirmedBlockMode::Execute,
1041        )
1042        .await?;
1043        Ok(())
1044    }
1045
1046    /// Tries to process all the certificates, requesting any missing blobs from the given nodes.
1047    /// Returns the chain info of the last successfully processed certificate.
1048    /// If `until_block_time` is `Some`, stops before processing any certificate whose
1049    /// block timestamp is greater or equal than the given value.
1050    #[instrument(level = "trace", skip_all)]
1051    async fn process_certificates(
1052        &self,
1053        remote_nodes: &[RemoteNode<Env::ValidatorNode>],
1054        certificates: Vec<ConfirmedBlockCertificate>,
1055        until_block_time: Option<Timestamp>,
1056        mode: ProcessConfirmedBlockMode,
1057    ) -> Result<Option<Box<ChainInfo>>, chain_client::Error> {
1058        let mut info = None;
1059        // Blobs created by these certs are already embedded in the downloaded
1060        // block bodies, so they don't need to be fetched from a validator. The
1061        // chain worker resolves them from `Block::created_blobs()` during
1062        // `handle_certificate`.
1063        let created_blob_ids = certificates
1064            .iter()
1065            .flat_map(|certificate| certificate.value().block().created_blob_ids())
1066            .collect::<BTreeSet<BlobId>>();
1067        let required_blob_ids = certificates
1068            .iter()
1069            .flat_map(|certificate| certificate.value().required_blob_ids())
1070            .filter(|blob_id| !created_blob_ids.contains(blob_id))
1071            .collect::<Vec<_>>();
1072
1073        match self
1074            .local_node
1075            .read_blob_states_from_storage(&required_blob_ids)
1076            .await
1077        {
1078            Err(LocalNodeError::BlobsNotFound(blob_ids)) => {
1079                self.download_blobs(remote_nodes, &blob_ids).await?;
1080            }
1081            x => {
1082                x?;
1083            }
1084        }
1085
1086        for certificate in certificates {
1087            if let Some(until) = until_block_time {
1088                if certificate.value().block().header.timestamp >= until {
1089                    break;
1090                }
1091            }
1092            let response = self
1093                .handle_certificate_with_retry(&certificate, remote_nodes, mode)
1094                .await?;
1095            info = Some(response.info);
1096        }
1097
1098        Ok(info)
1099    }
1100
1101    /// Calls `handle_confirmed_certificate`, retrying with any missing blobs (downloaded
1102    /// from `nodes`) and any missing events (downloaded from the publisher
1103    /// chains via the current validators).
1104    async fn handle_certificate_with_retry(
1105        &self,
1106        certificate: &ConfirmedBlockCertificate,
1107        nodes: &[RemoteNode<Env::ValidatorNode>],
1108        mode: ProcessConfirmedBlockMode,
1109    ) -> Result<ChainInfoResponse, chain_client::Error> {
1110        let mut downloaded_blobs = HashSet::<BlobId>::new();
1111        let mut downloaded_blocks = HashSet::<CryptoHash>::new();
1112        let mut events = EventSetDownloader::new(self);
1113        loop {
1114            let result = self
1115                .handle_confirmed_certificate(certificate.clone(), mode)
1116                .await;
1117            if let Err(LocalNodeError::BlobsNotFound(blob_ids)) = &result {
1118                let new_blobs = filter_new(blob_ids, &downloaded_blobs);
1119                if !new_blobs.is_empty() {
1120                    self.download_blobs(nodes, &new_blobs).await?;
1121                    downloaded_blobs.extend(new_blobs);
1122                    continue;
1123                }
1124            }
1125            if let Err(LocalNodeError::BlocksNotFound(hashes)) = &result {
1126                let new_blocks = filter_new(hashes, &downloaded_blocks);
1127                if !new_blocks.is_empty() {
1128                    self.download_pre_checkpoint_blocks(nodes, &new_blocks)
1129                        .await?;
1130                    downloaded_blocks.extend(new_blocks);
1131                    continue;
1132                }
1133            }
1134            if let Err(LocalNodeError::EventsNotFound(event_ids)) = &result {
1135                if events.download_new(event_ids).await? {
1136                    continue;
1137                }
1138            }
1139            return Ok(result?);
1140        }
1141    }
1142
1143    /// Downloads each missing pre-checkpoint sender block from `nodes` and feeds it
1144    /// through the local worker. The worker's trust-mark accept path verifies the
1145    /// cert against its own epoch's committee and writes it to storage. Routing
1146    /// through `handle_certificate_with_retry` ensures the sender block's own
1147    /// blob/event dependencies (e.g. a `ChainDescription` blob or an admin-chain
1148    /// epoch event for a revoked epoch) get resolved before the cert is accepted.
1149    async fn download_pre_checkpoint_blocks(
1150        &self,
1151        nodes: &[RemoteNode<Env::ValidatorNode>],
1152        hashes: &[CryptoHash],
1153    ) -> Result<(), chain_client::Error> {
1154        for hash in hashes {
1155            let mut last_error = None;
1156            for node in nodes {
1157                match node.node.download_certificate(*hash).await {
1158                    Ok(certificate) => {
1159                        Box::pin(self.handle_certificate_with_retry(
1160                            &certificate,
1161                            nodes,
1162                            ProcessConfirmedBlockMode::Auto,
1163                        ))
1164                        .await?;
1165                        last_error = None;
1166                        break;
1167                    }
1168                    Err(error) => last_error = Some(error),
1169                }
1170            }
1171            if let Some(error) = last_error {
1172                return Err(error.into());
1173            }
1174        }
1175        Ok(())
1176    }
1177
1178    async fn handle_certificate<T: ProcessableCertificate>(
1179        &self,
1180        certificate: T::Certificate,
1181    ) -> Result<ChainInfoResponse, LocalNodeError> {
1182        self.local_node
1183            .handle_certificate::<T>(certificate, &self.notifier)
1184            .await
1185    }
1186
1187    async fn handle_confirmed_certificate(
1188        &self,
1189        certificate: ConfirmedBlockCertificate,
1190        mode: ProcessConfirmedBlockMode,
1191    ) -> Result<ChainInfoResponse, LocalNodeError> {
1192        self.local_node
1193            .handle_confirmed_certificate(certificate, mode, &self.notifier)
1194            .await
1195    }
1196
1197    /// Obtains the committee for the latest epoch on the admin chain.
1198    pub async fn admin_committee(&self) -> Result<(Epoch, Arc<Committee>), LocalNodeError> {
1199        let info = self.local_node.chain_info(self.admin_chain_id).await?;
1200        let hash = info
1201            .committee_hash
1202            .ok_or(LocalNodeError::InactiveChain(self.admin_chain_id))?;
1203        let committee = self
1204            .storage_client()
1205            .get_or_load_committee_by_hash(hash)
1206            .await?;
1207        Ok((info.epoch, committee))
1208    }
1209
1210    /// Obtains the validators for the latest epoch.
1211    async fn validator_nodes(
1212        &self,
1213    ) -> Result<Vec<RemoteNode<Env::ValidatorNode>>, chain_client::Error> {
1214        let (_, committee) = self.admin_committee().await?;
1215        Ok(self.make_nodes(&committee)?)
1216    }
1217
1218    /// Creates a [`RemoteNode`] for each validator in the committee.
1219    fn make_nodes(
1220        &self,
1221        committee: &Committee,
1222    ) -> Result<Vec<RemoteNode<Env::ValidatorNode>>, NodeError> {
1223        Ok(self
1224            .validator_node_provider()
1225            .make_nodes(committee)?
1226            .map(|(public_key, node)| RemoteNode { public_key, node })
1227            .collect())
1228    }
1229
1230    /// Ensures that the client has the `ChainDescription` blob corresponding to this
1231    /// client's `ChainId`, and returns the chain description blob.
1232    pub async fn get_chain_description_blob(
1233        &self,
1234        chain_id: ChainId,
1235    ) -> Result<Arc<Blob>, chain_client::Error> {
1236        let chain_desc_id = BlobId::new(chain_id.0, BlobType::ChainDescription);
1237        let blob = self
1238            .local_node
1239            .storage_client()
1240            .read_blob(chain_desc_id)
1241            .await?;
1242        if let Some(blob) = blob {
1243            // We have the blob - return it.
1244            return Ok(blob.into_std());
1245        }
1246        // Recover history from the current validators, according to the admin chain.
1247        self.synchronize_chain_state(self.admin_chain_id).await?;
1248        let nodes = self.validator_nodes().await?;
1249        Ok(self
1250            .update_local_node_with_blobs_from(vec![chain_desc_id], &nodes)
1251            .await?
1252            .pop()
1253            .unwrap() // Returns exactly as many blobs as passed-in IDs.
1254            .into_std())
1255    }
1256
1257    /// Ensures that the client has the `ChainDescription` blob corresponding to this
1258    /// client's `ChainId`, and returns the chain description.
1259    pub async fn get_chain_description(
1260        &self,
1261        chain_id: ChainId,
1262    ) -> Result<ChainDescription, chain_client::Error> {
1263        let blob = self.get_chain_description_blob(chain_id).await?;
1264        Ok(bcs::from_bytes(blob.bytes())?)
1265    }
1266
1267    /// Ensures that the client has the `ApplicationDescription` blob for the given
1268    /// application ID, fetching it from the current validators if it is not available
1269    /// locally, and returns the blob. The application need not be registered on this
1270    /// client's chain: the description is content-addressed and downloaded by blob ID.
1271    pub async fn get_application_description_blob(
1272        &self,
1273        application_id: ApplicationId,
1274    ) -> Result<Arc<Blob>, chain_client::Error> {
1275        let blob_id = application_id.description_blob_id();
1276        let blob = self.local_node.storage_client().read_blob(blob_id).await?;
1277        if let Some(blob) = blob {
1278            // We have the blob - return it.
1279            return Ok(blob.into_std());
1280        }
1281        // Recover the blob from the current validators, according to the admin chain.
1282        Box::pin(self.synchronize_chain_state(self.admin_chain_id)).await?;
1283        let nodes = self.validator_nodes().await?;
1284        Ok(self
1285            .update_local_node_with_blobs_from(vec![blob_id], &nodes)
1286            .await?
1287            .pop()
1288            .unwrap() // Returns exactly as many blobs as passed-in IDs.
1289            .into_std())
1290    }
1291
1292    /// Returns the `ApplicationDescription` of the given application, fetching its
1293    /// description blob from the validators if it is not available locally.
1294    pub async fn get_application_description(
1295        &self,
1296        application_id: ApplicationId,
1297    ) -> Result<ApplicationDescription, chain_client::Error> {
1298        let blob = self
1299            .get_application_description_blob(application_id)
1300            .await?;
1301        Ok(bcs::from_bytes(blob.bytes())?)
1302    }
1303
1304    /// Submits a validated block for finalization and returns the confirmed block certificate.
1305    #[instrument(level = "trace", skip_all)]
1306    pub(crate) async fn finalize_block(
1307        self: &Arc<Self>,
1308        committee: &Committee,
1309        certificate: ValidatedBlockCertificate,
1310    ) -> Result<ConfirmedBlockCertificate, chain_client::Error> {
1311        debug!(round = %certificate.round(), "Submitting block for confirmation");
1312        let hashed_value = ConfirmedBlock::new(certificate.block().clone());
1313        // The full chain of validated quorums for the block: this validated certificate's own
1314        // quorum as the top link, then the chain below it. Whether the confirmed certificate
1315        // actually carries it is decided *after* the quorum forms, from the attestation the
1316        // confirming votes signed (below).
1317        let full_justification = certificate.full_justification();
1318        let finalize_action = CommunicateAction::FinalizeBlock {
1319            certificate: Box::new(certificate),
1320            delivery: self.options.cross_chain_message_delivery,
1321        };
1322        let quorum = self
1323            .communicate_chain_action(committee, finalize_action, hashed_value)
1324            .await?;
1325        // Omit the chain iff the confirming votes attested that this is the chain's first round:
1326        // such a block is always the lower one in any fork, so it never needs a chain of its own.
1327        // Deciding from the quorum's signed attestation — rather than our local ownership view,
1328        // which a concurrently finalized block could have advanced to a different first round —
1329        // guarantees the certificate we assemble matches what the validators actually signed.
1330        let justification = if quorum.first_round() {
1331            JustificationChain::default()
1332        } else {
1333            full_justification
1334        };
1335        // The confirming votes committed to the chain they were shown; the chain we attach must
1336        // be that one, or the assembled certificate would fail verification everywhere.
1337        ensure!(
1338            quorum.justification_commitment() == justification.commitment(quorum.hash()),
1339            chain_client::Error::ProtocolError(
1340                "A quorum confirmed with a justification commitment that does not match the \
1341                 validated certificate's justification chain",
1342            )
1343        );
1344        let certificate = ConfirmedBlockCertificate::from_parts(quorum, justification);
1345        self.receive_certificate_with_checked_signatures(
1346            certificate.clone(),
1347            ProcessConfirmedBlockMode::Execute,
1348        )
1349        .await?;
1350        Ok(certificate)
1351    }
1352
1353    /// Submits a block proposal to the validators.
1354    #[instrument(level = "trace", skip_all)]
1355    async fn submit_block_proposal<T: ProcessableCertificate>(
1356        self: &Arc<Self>,
1357        committee: Arc<Committee>,
1358        proposal: Box<BlockProposal>,
1359        value: T,
1360    ) -> Result<T::Certificate, chain_client::Error> {
1361        debug!(
1362            round = %proposal.content.round,
1363            "Submitting block proposal to validators"
1364        );
1365
1366        // The certificate's justification chain comes from the proposal: a regular retry is
1367        // justified by the validated certificate it carries (the new top link plus that
1368        // certificate's own chain); a fresh proposal or fast-round proposal has none.
1369        let justification = match proposal.original_proposal.as_ref() {
1370            Some(OriginalProposal::Regular { certificate }) => certificate.full_justification(),
1371            Some(OriginalProposal::Fast(_)) | None => JustificationChain::default(),
1372        };
1373
1374        // Check if the block timestamp is in the future and log INFO.
1375        let block_timestamp = proposal.content.block.timestamp;
1376        let local_time = self.local_node.storage_client().clock().current_time();
1377        if block_timestamp > local_time {
1378            info!(
1379                chain_id = %proposal.content.block.chain_id,
1380                %block_timestamp,
1381                %local_time,
1382                "Block timestamp is in the future; waiting until it can be proposed",
1383            );
1384        }
1385
1386        // Create channel for clock skew reports from validators.
1387        let (clock_skew_sender, mut clock_skew_receiver) = mpsc::unbounded_channel();
1388        let submit_action = CommunicateAction::SubmitBlock {
1389            proposal,
1390            blob_ids: value.required_blob_ids().into_iter().collect(),
1391            clock_skew_sender,
1392        };
1393
1394        // Spawn a task to monitor clock skew reports and warn if threshold is reached.
1395        let validity_threshold = committee.validity_threshold();
1396        let committee_clone = committee.clone();
1397        let clock_skew_check_handle = linera_base::Task::spawn(async move {
1398            let mut skew_weight = 0u64;
1399            let mut min_skew = TimeDelta::MAX;
1400            let mut max_skew = TimeDelta::ZERO;
1401            while let Some((public_key, clock_skew)) = clock_skew_receiver.recv().await {
1402                if clock_skew.as_micros() > 0 {
1403                    skew_weight += committee_clone.weight(&public_key);
1404                    min_skew = min_skew.min(clock_skew);
1405                    max_skew = max_skew.max(clock_skew);
1406                    if skew_weight >= validity_threshold {
1407                        warn!(
1408                            skew_weight,
1409                            validity_threshold,
1410                            min_skew_ms = min_skew.as_micros() / 1000,
1411                            max_skew_ms = max_skew.as_micros() / 1000,
1412                            "A validity threshold of validators reported clock skew; \
1413                             consider checking your system clock",
1414                        );
1415                        return;
1416                    }
1417                }
1418            }
1419        });
1420
1421        let quorum = self
1422            .communicate_chain_action(&committee, submit_action, value)
1423            .await?;
1424
1425        clock_skew_check_handle.await;
1426
1427        // The justification chain comes from our own proposal, but the winning quorum may have
1428        // been formed from a competing proposal that cited a different certificate: its votes then
1429        // sign a different unlocking round and justification commitment, and gluing our chain onto
1430        // them would build a certificate that fails verification downstream. Reject that here with
1431        // a retryable error rather than assembling a mismatched certificate. (For confirmed and
1432        // timeout quorums both sides are `None`, so this only bites the validated-retry case it is
1433        // meant to guard.)
1434        ensure!(
1435            quorum.unlocking_round() == justification.top_unlocking_round(),
1436            chain_client::Error::ProtocolError(
1437                "A quorum voted with an unlocking round that does not match the proposal's \
1438                 justification chain",
1439            )
1440        );
1441        ensure!(
1442            quorum.justification_commitment() == justification.commitment(quorum.hash()),
1443            chain_client::Error::ProtocolError(
1444                "A quorum voted with a justification commitment that does not match the \
1445                 proposal's justification chain",
1446            )
1447        );
1448        let certificate = T::make_certificate(quorum, justification);
1449        self.handle_certificate::<T>(certificate.clone()).await?;
1450        Ok(certificate)
1451    }
1452
1453    /// Creates a [`RemoteNodeUpdater`] for the given validator, backed by our local node.
1454    fn remote_node_updater(
1455        &self,
1456        remote_node: RemoteNode<Env::ValidatorNode>,
1457    ) -> RemoteNodeUpdater<Env::Storage, Env::ValidatorNode> {
1458        RemoteNodeUpdater {
1459            remote_node,
1460            local_node: self.local_node.clone(),
1461            admin_chain_id: self.admin_chain_id,
1462            certificate_upload_batch_size: self.options.certificate_upload_batch_size,
1463        }
1464    }
1465
1466    /// Pulls the chain state that validators reported being ahead of the local node on,
1467    /// during a failed quorum round.
1468    ///
1469    /// Reports are processed here, after `communicate_with_quorum` returns, rather than inside
1470    /// the per-validator tasks: this keeps those tasks read-only for the local node — mutually
1471    /// independent and fair to each validator — and means a pull can no longer be cancelled by
1472    /// the quorum's early exit. Processing is best-effort: failures are only logged, and the
1473    /// quorum outcome is surfaced unchanged either way, so the outer logic reacts on top of
1474    /// whatever state was absorbed (e.g. `execute_operations` rebuilds and re-proposes).
1475    ///
1476    /// Validators that are further ahead are pulled from first, which makes later pulls cheap,
1477    /// but every reporter is visited: a proposal rejection justified by a locking block can
1478    /// only be absorbed from a validator that holds that block.
1479    async fn process_lag_reports(&self, mut reports: Vec<LagReport<Env::ValidatorNode>>) {
1480        reports.sort_by_key(|report| Reverse(report.remote_progress()));
1481        for report in reports {
1482            // Boxed to keep this future small: the synchronization future is large, and it
1483            // would otherwise be inlined into every caller of the quorum communication.
1484            if let Err(error) =
1485                Box::pin(self.synchronize_chain_state_from(&report.remote_node, report.chain_id))
1486                    .await
1487            {
1488                debug!(
1489                    remote_node = report.remote_node.address(),
1490                    chain_id = %report.chain_id,
1491                    %error,
1492                    "failed to pull chain state from a validator that reported being ahead",
1493                );
1494            }
1495        }
1496    }
1497
1498    /// Broadcasts certified blocks to validators.
1499    #[instrument(level = "trace", skip_all, fields(chain_id, block_height, delivery))]
1500    async fn communicate_chain_updates(
1501        self: &Arc<Self>,
1502        committee: &Committee,
1503        chain_id: ChainId,
1504        height: BlockHeight,
1505        delivery: CrossChainMessageDelivery,
1506        latest_certificate: Option<CacheArc<ConfirmedBlockCertificate>>,
1507    ) -> Result<(), chain_client::Error> {
1508        let nodes = self.make_nodes(committee)?;
1509        communicate_with_quorum(
1510            &nodes,
1511            committee,
1512            |_: &()| (),
1513            |remote_node| {
1514                let mut updater = self.remote_node_updater(remote_node);
1515                let certificate = latest_certificate.clone();
1516                Box::pin(async move {
1517                    updater
1518                        .send_chain_information(chain_id, height, delivery, certificate)
1519                        .await
1520                })
1521            },
1522            self.options.quorum_grace_period,
1523        )
1524        .await?;
1525        Ok(())
1526    }
1527
1528    /// Broadcasts certified blocks and optionally a block proposal, certificate or
1529    /// leader timeout request.
1530    ///
1531    /// In that case, it verifies that the validator votes are for the provided value,
1532    /// and returns a certificate.
1533    #[instrument(level = "trace", skip_all)]
1534    async fn communicate_chain_action<T: CertificateValue>(
1535        self: &Arc<Self>,
1536        committee: &Committee,
1537        action: CommunicateAction,
1538        value: T,
1539    ) -> Result<GenericCertificate<T>, chain_client::Error> {
1540        // Validators that turn out to be ahead of the local node are recorded here and pulled
1541        // from after the quorum round, so that each per-validator task stays read-only for the
1542        // local node. The updater's original validator error re-enters the quorum aggregation
1543        // below, keeping the round's error classification unchanged.
1544        let lag_reports = Mutex::new(Vec::new());
1545        let nodes = self.make_nodes(committee)?;
1546        // Group votes by their full signed payload: signatures only aggregate into a
1547        // certificate if they are unanimous on all signed fields, so a vote that diverges in
1548        // the unlocking round, first-round attestation or justification commitment belongs to
1549        // a separate candidate quorum.
1550        let result = communicate_with_quorum(
1551            &nodes,
1552            committee,
1553            |vote: &LiteVote| {
1554                (
1555                    vote.value.value_hash,
1556                    vote.round,
1557                    vote.unlocking_round,
1558                    vote.first_round,
1559                    vote.justification_commitment,
1560                )
1561            },
1562            |remote_node| {
1563                let mut updater = self.remote_node_updater(remote_node.clone());
1564                let action = action.clone();
1565                let lag_reports = &lag_reports;
1566                Box::pin(async move {
1567                    match updater.send_chain_update(action).await {
1568                        Err(chain_client::Error::LocalNodeLagging { chain_id, error }) => {
1569                            lag_reports.lock().unwrap().push(LagReport {
1570                                remote_node,
1571                                chain_id,
1572                                error: (*error).clone(),
1573                            });
1574                            Err((*error).into())
1575                        }
1576                        result => result,
1577                    }
1578                })
1579            },
1580            self.options.quorum_grace_period,
1581        )
1582        .await;
1583        let ((votes_hash, votes_round, _, _, _), votes) = match result {
1584            Ok(quorum) => quorum,
1585            Err(err) => {
1586                // The round failed; absorb whatever the more advanced validators hold before
1587                // surfacing the outcome, so the caller retries on top of a synchronized state.
1588                self.process_lag_reports(lag_reports.into_inner().unwrap())
1589                    .await;
1590                return Err(err.into());
1591            }
1592        };
1593        ensure!(
1594            (votes_hash, votes_round) == (value.hash(), action.round()),
1595            chain_client::Error::UnexpectedQuorum {
1596                hash: votes_hash,
1597                round: votes_round,
1598                expected_hash: value.hash(),
1599                expected_round: action.round(),
1600            }
1601        );
1602        // Certificate is valid because
1603        // * `communicate_with_quorum` ensured a sufficient "weight" of
1604        // (non-error) answers were returned by validators.
1605        // * each answer is a vote signed by the expected validator.
1606        let certificate = LiteCertificate::try_from_votes(votes)
1607            .ok_or_else(|| {
1608                chain_client::Error::InternalError(
1609                    "Vote values or rounds don't match; this is a bug",
1610                )
1611            })?
1612            .with_value(value)
1613            .ok_or_else(|| {
1614                chain_client::Error::ProtocolError("A quorum voted for an unexpected value")
1615            })?;
1616        Ok(certificate)
1617    }
1618
1619    /// Processes the confirmed block certificate in the local node without checking signatures.
1620    /// Also downloads and processes all ancestors that are still missing.
1621    #[instrument(level = "trace", skip_all)]
1622    async fn receive_certificate_with_checked_signatures(
1623        &self,
1624        certificate: ConfirmedBlockCertificate,
1625        mode: ProcessConfirmedBlockMode,
1626    ) -> Result<(), chain_client::Error> {
1627        let block = certificate.block();
1628        // Recover history from the network.
1629        self.download_certificates(block.header.chain_id, block.header.height)
1630            .await?;
1631        // Process the received operations. Download required hashed certificate values if
1632        // necessary.
1633        let nodes = self.validator_nodes().await?;
1634        self.handle_certificate_with_retry(&certificate, &nodes, mode)
1635            .await?;
1636        Ok(())
1637    }
1638
1639    /// Processes the confirmed block in the local node. Whether it is fully
1640    /// executed or only preprocessed depends on the chain's [`ListeningMode`]:
1641    /// chains we follow get executed, untracked or events-only chains are only
1642    /// preprocessed.
1643    #[instrument(level = "trace", skip_all)]
1644    // Returns a boxed future rather than being an `async fn`: this function recurses
1645    // (admin-chain sync -> sender-certificate processing -> here), and boxing it at this
1646    // boundary lets the concurrent admin-chain self-heal below satisfy its future bound
1647    // despite the recursion. The future is `Send` off `web` (see the type alias).
1648    fn receive_sender_certificate(
1649        &self,
1650        certificate: CacheArc<ConfirmedBlockCertificate>,
1651        mode: ReceiveCertificateMode,
1652        nodes: Option<Vec<RemoteNode<Env::ValidatorNode>>>,
1653    ) -> ReceiveSenderCertificateFuture<'_> {
1654        Box::pin(async move {
1655            // Verify the certificate before doing any expensive networking.
1656            if let ReceiveCertificateMode::NeedsCheck = mode {
1657                let mut check_result = self.check_certificate(&certificate).await?;
1658                if matches!(check_result, CheckCertificateResult::FutureEpoch) {
1659                    // The certificate is from an epoch our local view of the admin chain
1660                    // hasn't caught up to yet. Catch up and check again instead of failing.
1661                    // Prefer the nodes that gave us the certificate: they evidently know
1662                    // the newer epoch even if our own committee view is stale or its
1663                    // members are unreachable. Race them concurrently rather than blocking
1664                    // on a slow one, then fall back to the known committee. A sync only
1665                    // counts if it actually made the epoch known.
1666                    let admin_chain_id = self.admin_chain_id;
1667                    let epoch = certificate.block().header.epoch;
1668                    info!(
1669                        %epoch,
1670                        "certificate is from an unknown epoch; synchronizing the admin chain"
1671                    );
1672                    let synced_from_serving_node = if let Some(nodes) = &nodes {
1673                        let certificate = &certificate;
1674                        communicate_concurrently(
1675                            nodes,
1676                            |node| {
1677                                Box::pin(async move {
1678                                    self.synchronize_chain_state_from(&node, admin_chain_id)
1679                                        .await?;
1680                                    match self.check_certificate(certificate).await? {
1681                                        CheckCertificateResult::FutureEpoch => {
1682                                            Err(chain_client::Error::CommitteeSynchronizationError)
1683                                        }
1684                                        _ => Ok(()),
1685                                    }
1686                                })
1687                            },
1688                            self.options.blob_download_hedge_delay,
1689                            self.storage_client().clock(),
1690                        )
1691                        .await
1692                        .is_ok()
1693                    } else {
1694                        false
1695                    };
1696                    if synced_from_serving_node {
1697                        check_result = self.check_certificate(&certificate).await?;
1698                    }
1699                    if matches!(check_result, CheckCertificateResult::FutureEpoch) {
1700                        Box::pin(self.synchronize_chain_state(admin_chain_id)).await?;
1701                        check_result = self.check_certificate(&certificate).await?;
1702                    }
1703                }
1704                check_result.into_result()?;
1705            }
1706            // Recover history from the network.
1707            let nodes = if let Some(nodes) = nodes {
1708                nodes
1709            } else {
1710                self.validator_nodes().await?
1711            };
1712            let processing_mode = if self
1713                .chain_mode(certificate.value().chain_id())
1714                .is_some_and(|m| m.should_sync_chain_state())
1715            {
1716                ProcessConfirmedBlockMode::Auto
1717            } else {
1718                ProcessConfirmedBlockMode::Preprocess
1719            };
1720            self.handle_certificate_with_retry(&certificate, &nodes, processing_mode)
1721                .await?;
1722
1723            Ok(())
1724        })
1725    }
1726
1727    /// Downloads and processes certificates for sender chain blocks.
1728    #[instrument(level = "debug", skip_all, fields(chain_id = %sender_chain_id))]
1729    async fn download_and_process_sender_chain(
1730        &self,
1731        sender_chain_id: ChainId,
1732        nodes: &[RemoteNode<Env::ValidatorNode>],
1733        received_log: &ReceivedLogs,
1734        mut remote_heights: Vec<BlockHeight>,
1735        sender: mpsc::UnboundedSender<ChainAndHeight>,
1736    ) {
1737        let mut nodes = nodes.to_vec();
1738        while !remote_heights.is_empty() {
1739            // Check local storage first — certificates may already be available from
1740            // a prior sync cycle, another receiver chain, or a concurrent notification.
1741            if let Ok(local_certs) = self
1742                .storage_client()
1743                .read_certificates_by_heights(sender_chain_id, &remote_heights)
1744                .await
1745            {
1746                let mut still_needed = Vec::new();
1747                for (height, maybe_cert) in remote_heights.iter().copied().zip(local_certs) {
1748                    if let Some(certificate) = maybe_cert {
1749                        let chain_id = certificate.block().header.chain_id;
1750                        if let Err(error) = sender.send(ChainAndHeight { chain_id, height }) {
1751                            error!(
1752                                %chain_id, %height, %error,
1753                                "failed to send chain and height over the channel",
1754                            );
1755                        }
1756                    } else {
1757                        still_needed.push(height);
1758                    }
1759                }
1760                remote_heights = still_needed;
1761                if remote_heights.is_empty() {
1762                    break;
1763                }
1764            }
1765
1766            let remote_heights_ref = &remote_heights;
1767            let certificates = match communicate_concurrently(
1768                &nodes,
1769                async move |remote_node| {
1770                    let mut remote_heights = remote_heights_ref.clone();
1771                    // No need trying to download certificates the validator didn't have in their
1772                    // log - we'll retry downloading the remaining ones next time we loop.
1773                    remote_heights.retain(|height| {
1774                        received_log.validator_has_block(
1775                            &remote_node.public_key,
1776                            sender_chain_id,
1777                            *height,
1778                        )
1779                    });
1780                    if remote_heights.is_empty() {
1781                        // It makes no sense to return `Ok(_)` if we aren't going to try downloading
1782                        // anything from the validator - let the function try the other validators
1783                        return Err(NodeError::MissingCertificateValue);
1784                    }
1785                    let certificates = self
1786                        .requests_scheduler
1787                        .download_certificates_by_heights(
1788                            &remote_node,
1789                            sender_chain_id,
1790                            remote_heights,
1791                        )
1792                        .await?;
1793                    let mut certificates_with_check_results = vec![];
1794                    for cert in certificates {
1795                        let check_result = self.check_certificate(&cert).await?;
1796                        certificates_with_check_results
1797                            .push((cert, check_result.into_result().is_ok()));
1798                    }
1799                    Ok(certificates_with_check_results)
1800                },
1801                self.options.certificate_batch_download_hedge_delay,
1802                self.storage_client().clock(),
1803            )
1804            .await
1805            {
1806                Ok(certificates_with_check_results) => certificates_with_check_results,
1807                Err(errors) => {
1808                    let faulty_validators = errors
1809                        .into_iter()
1810                        .map(|(validator, error)| {
1811                            warn!(
1812                                %validator,
1813                                %sender_chain_id,
1814                                %error,
1815                                "failed to download certificates from validator",
1816                            );
1817                            validator
1818                        })
1819                        .collect::<BTreeSet<_>>();
1820                    // filter out faulty validators and retry if any are left
1821                    nodes.retain(|node| !faulty_validators.contains(&node.public_key));
1822                    if nodes.is_empty() {
1823                        info!(
1824                            chain_id = %sender_chain_id,
1825                            "could not download certificates for chain - no more correct validators left"
1826                        );
1827                        return;
1828                    }
1829                    continue;
1830                }
1831            };
1832
1833            trace!(
1834                num_certificates = %certificates.len(),
1835                "received certificates",
1836            );
1837
1838            let mut to_remove_from_queue = BTreeSet::new();
1839
1840            for (certificate, check_result) in certificates {
1841                let hash = certificate.hash();
1842                let chain_id = certificate.block().header.chain_id;
1843                let height = certificate.block().header.height;
1844                if !check_result {
1845                    // The certificate was correctly signed, but we were missing a committee to
1846                    // validate it properly - do not receive it, but also do not attempt to
1847                    // re-download it.
1848                    to_remove_from_queue.insert(height);
1849                    continue;
1850                }
1851                // We checked the certificates right after downloading them.
1852                let mode = ReceiveCertificateMode::AlreadyChecked;
1853                if let Err(error) = self
1854                    .receive_sender_certificate(
1855                        self.storage_client().cache_certificate(certificate),
1856                        mode,
1857                        None,
1858                    )
1859                    .await
1860                {
1861                    warn!(%error, %hash, "Received invalid certificate");
1862                } else {
1863                    to_remove_from_queue.insert(height);
1864                    if let Err(error) = sender.send(ChainAndHeight { chain_id, height }) {
1865                        error!(
1866                            %chain_id,
1867                            %height,
1868                            %error,
1869                            "failed to send chain and height over the channel",
1870                        );
1871                    }
1872                }
1873            }
1874
1875            remote_heights.retain(|height| !to_remove_from_queue.contains(height));
1876        }
1877        trace!("find_received_certificates: finished processing chain");
1878    }
1879
1880    /// Downloads the log of received messages for a chain from a validator.
1881    #[instrument(level = "trace", skip(self))]
1882    async fn get_received_log_from_validator(
1883        &self,
1884        chain_id: ChainId,
1885        remote_node: &RemoteNode<Env::ValidatorNode>,
1886        tracker: u64,
1887    ) -> Result<Vec<ChainAndHeight>, chain_client::Error> {
1888        let mut offset = tracker;
1889
1890        // Retrieve the list of newly received certificates from this validator.
1891        let mut remote_log = Vec::new();
1892        loop {
1893            trace!("get_received_log_from_validator: looping");
1894            let query = ChainInfoQuery::new(chain_id).with_received_log_excluding_first_n(offset);
1895            let info = remote_node.handle_chain_info_query(query).await?;
1896            let received_entries = info.requested_received_log.len();
1897            offset += received_entries as u64;
1898            remote_log.extend(info.requested_received_log);
1899            trace!(
1900                remote_node = remote_node.address(),
1901                %received_entries,
1902                "get_received_log_from_validator: received log batch",
1903            );
1904            if received_entries < CHAIN_INFO_MAX_RECEIVED_LOG_ENTRIES {
1905                break;
1906            }
1907        }
1908
1909        trace!(
1910            remote_node = remote_node.address(),
1911            num_entries = remote_log.len(),
1912            "get_received_log_from_validator: returning downloaded log",
1913        );
1914
1915        Ok(remote_log)
1916    }
1917
1918    /// Downloads a specific sender block and recursively downloads any earlier blocks
1919    /// that also sent a message to our chain, based on `previous_message_blocks`.
1920    ///
1921    /// This ensures that we have all the sender blocks needed to preprocess the target block
1922    /// and put the messages to our chain into the outbox.
1923    async fn download_sender_block_with_sending_ancestors(
1924        &self,
1925        receiver_chain_id: ChainId,
1926        sender_chain_id: ChainId,
1927        height: BlockHeight,
1928        remote_node: &RemoteNode<Env::ValidatorNode>,
1929    ) -> Result<(), chain_client::Error> {
1930        let next_outbox_height = self
1931            .local_node
1932            .next_outbox_heights(&[sender_chain_id], receiver_chain_id)
1933            .await?
1934            .get(&sender_chain_id)
1935            .copied()
1936            .unwrap_or(BlockHeight::ZERO);
1937
1938        // Recursively collect all certificates we need, following
1939        // the chain of previous_message_blocks back to next_outbox_height.
1940        let mut certificates = BTreeMap::new();
1941        let mut current_height = height;
1942        // On the first iteration we only have a height; subsequent iterations
1943        // also carry the hash from `previous_message_blocks`.
1944        let mut current_hash: Option<CryptoHash> = None;
1945
1946        // Stop if we've reached the height we've already processed.
1947        while current_height >= next_outbox_height {
1948            // Try local storage first — avoids a validator round-trip when
1949            // the certificate was already downloaded by a prior sync cycle,
1950            // another receiver chain, or a concurrent notification handler.
1951            let certificate = if let Some(local) = self
1952                .try_read_local_certificate(sender_chain_id, current_height, current_hash)
1953                .await?
1954            {
1955                local
1956            } else {
1957                let downloaded = self
1958                    .requests_scheduler
1959                    .download_certificates_by_heights(
1960                        remote_node,
1961                        sender_chain_id,
1962                        vec![current_height],
1963                    )
1964                    .await?;
1965                let Some(certificate) = downloaded.into_iter().next() else {
1966                    return Err(chain_client::Error::CannotDownloadMissingSenderBlock {
1967                        chain_id: sender_chain_id,
1968                        height: current_height,
1969                    });
1970                };
1971                self.storage_client().cache_certificate(certificate)
1972            };
1973
1974            // Validate the certificate.
1975            self.check_certificate(&certificate).await?.into_result()?;
1976
1977            // Check if there's a previous message block to our chain.
1978            let block = certificate.block();
1979            let next = block
1980                .body
1981                .previous_message_blocks
1982                .get(&receiver_chain_id)
1983                .map(|(prev_hash, prev_height)| (*prev_hash, *prev_height));
1984
1985            // Store this certificate.
1986            certificates.insert(current_height, certificate);
1987
1988            if let Some((prev_hash, prev_height)) = next {
1989                // Continue with the previous block (now with its hash for local lookup).
1990                current_height = prev_height;
1991                current_hash = Some(prev_hash);
1992            } else {
1993                // No more dependencies.
1994                break;
1995            }
1996        }
1997
1998        if certificates.is_empty() {
1999            self.retry_pending_cross_chain_requests(sender_chain_id)
2000                .await?;
2001        }
2002
2003        // Process certificates in ascending block height order (BTreeMap keeps them sorted).
2004        for certificate in certificates.into_values() {
2005            self.receive_sender_certificate(
2006                certificate,
2007                ReceiveCertificateMode::AlreadyChecked,
2008                Some(vec![remote_node.clone()]),
2009            )
2010            .await?;
2011        }
2012
2013        Ok(())
2014    }
2015
2016    /// Downloads event-bearing blocks for the given streams by walking the
2017    /// `previous_event_blocks` linked list backwards from `height`, stopping when we
2018    /// reach blocks that are already executed locally or whose events we already track.
2019    async fn download_event_bearing_blocks(
2020        &self,
2021        publisher_chain_id: ChainId,
2022        initial_blocks: BTreeSet<(BlockHeight, CryptoHash)>,
2023        local_next_block_height: BlockHeight,
2024        subscribed_streams: &BTreeSet<StreamId>,
2025        remote_node: &RemoteNode<Env::ValidatorNode>,
2026    ) -> Result<(), chain_client::Error> {
2027        if initial_blocks.is_empty() {
2028            return Ok(());
2029        }
2030
2031        let mut certificates = BTreeMap::new();
2032        let mut blocks_to_fetch = initial_blocks;
2033        let next_expected_events = self
2034            .local_node
2035            .next_expected_events(
2036                publisher_chain_id,
2037                subscribed_streams.iter().cloned().collect(),
2038            )
2039            .await?;
2040
2041        while let Some((current_height, current_hash)) = blocks_to_fetch.pop_last() {
2042            if current_height < local_next_block_height {
2043                continue; // Already executed locally.
2044            }
2045            if certificates.contains_key(&current_height) {
2046                continue;
2047            }
2048
2049            let certificate = if let Some(certificate) =
2050                self.storage_client().read_certificate(current_hash).await?
2051            {
2052                certificate
2053            } else {
2054                let downloaded = self
2055                    .requests_scheduler
2056                    .download_certificates(remote_node, publisher_chain_id, current_height, 1)
2057                    .await?;
2058                let Some(certificate) = downloaded.into_iter().next() else {
2059                    tracing::debug!(
2060                        validator = remote_node.address(),
2061                        %publisher_chain_id,
2062                        height = %current_height,
2063                        "failed to download event publisher block"
2064                    );
2065                    continue;
2066                };
2067
2068                self.check_certificate(&certificate).await?.into_result()?;
2069
2070                self.storage_client().cache_certificate(certificate)
2071            };
2072
2073            let block = certificate.block();
2074            // Walk previous_event_blocks for subscribed streams.
2075            for stream_id in subscribed_streams {
2076                if let Some((prev_hash, prev_height)) =
2077                    block.body.previous_event_blocks.get(stream_id)
2078                {
2079                    if next_expected_events.get(stream_id).is_some_and(|index| {
2080                        block
2081                            .body
2082                            .events
2083                            .iter()
2084                            .flatten()
2085                            .find(|event| event.stream_id == *stream_id)
2086                            .is_some_and(|event| event.index == *index)
2087                    }) {
2088                        continue;
2089                    }
2090                    if !certificates.contains_key(prev_height) {
2091                        blocks_to_fetch.insert((*prev_height, *prev_hash));
2092                    }
2093                }
2094            }
2095
2096            certificates.insert(current_height, certificate);
2097        }
2098
2099        // Process in ascending height order.
2100        for certificate in certificates.into_values() {
2101            self.receive_sender_certificate(
2102                certificate,
2103                ReceiveCertificateMode::AlreadyChecked,
2104                Some(vec![remote_node.clone()]),
2105            )
2106            .await?;
2107        }
2108
2109        Ok(())
2110    }
2111
2112    /// Queries a validator for event-bearing blocks for the given streams, then downloads
2113    /// them.
2114    async fn sync_events_from_node(
2115        &self,
2116        chain_id: ChainId,
2117        stream_ids: &BTreeSet<StreamId>,
2118        remote_node: &RemoteNode<Env::ValidatorNode>,
2119    ) -> Result<(), chain_client::Error> {
2120        let stream_ids_vec = stream_ids.iter().cloned().collect::<Vec<_>>();
2121        let mut initial_blocks = BTreeSet::new();
2122        for chunk in stream_ids_vec.chunks(self.options.max_event_stream_queries) {
2123            let query = ChainInfoQuery::new(chain_id).with_previous_event_blocks(chunk.to_vec());
2124            let info = remote_node.handle_chain_info_query(query).await?;
2125            initial_blocks.extend(info.requested_previous_event_blocks.values().copied());
2126        }
2127        let local_height = match self.local_node.chain_info(chain_id).await {
2128            Ok(info) => info.next_block_height,
2129            Err(LocalNodeError::InactiveChain(_) | LocalNodeError::BlobsNotFound(_)) => {
2130                BlockHeight::ZERO
2131            }
2132            Err(error) => return Err(error.into()),
2133        };
2134        self.download_event_bearing_blocks(
2135            chain_id,
2136            initial_blocks,
2137            local_height,
2138            stream_ids,
2139            remote_node,
2140        )
2141        .await
2142    }
2143
2144    #[instrument(
2145        level = "trace", skip_all,
2146        fields(certificate_hash = ?incoming_certificate.hash()),
2147    )]
2148    async fn check_certificate(
2149        &self,
2150        incoming_certificate: &ConfirmedBlockCertificate,
2151    ) -> Result<CheckCertificateResult, NodeError> {
2152        let epoch = incoming_certificate.block().header.epoch;
2153        let storage = self.storage_client();
2154        let view_err = |error: ExecutionError| NodeError::ViewError {
2155            error: error.to_string(),
2156        };
2157        if storage.is_epoch_revoked(epoch).await.map_err(view_err)? {
2158            return Ok(CheckCertificateResult::OldEpoch);
2159        }
2160        let Some(committee) = storage.committee_for_epoch(epoch).await.map_err(view_err)? else {
2161            return Ok(CheckCertificateResult::FutureEpoch);
2162        };
2163        incoming_certificate.check(&committee)?;
2164        Ok(CheckCertificateResult::New)
2165    }
2166
2167    /// Downloads and processes any certificates we are missing for the given chain.
2168    ///
2169    /// Whether manager values are fetched depends on the chain's follow-only state.
2170    #[instrument(level = "trace", skip_all)]
2171    async fn synchronize_chain_state(
2172        &self,
2173        chain_id: ChainId,
2174    ) -> Result<Box<ChainInfo>, chain_client::Error> {
2175        let (_, committee) = self.admin_committee().await?;
2176        self.synchronize_chain_from_committee(chain_id, committee)
2177            .await
2178    }
2179
2180    /// Downloads certificates for the given chain from the given committee.
2181    ///
2182    /// If the chain is not in follow-only mode, also fetches and processes manager values
2183    /// (timeout certificates, proposals, locking blocks) for consensus participation.
2184    #[instrument(level = "trace", skip_all)]
2185    pub(crate) async fn synchronize_chain_from_committee(
2186        &self,
2187        chain_id: ChainId,
2188        committee: Arc<Committee>,
2189    ) -> Result<Box<ChainInfo>, chain_client::Error> {
2190        #[cfg(with_metrics)]
2191        let _latency = if !self.is_chain_follow_only(chain_id) {
2192            Some(metrics::SYNCHRONIZE_CHAIN_STATE_LATENCY.measure_latency())
2193        } else {
2194            None
2195        };
2196
2197        let validators = self.make_nodes(&committee)?;
2198        Box::pin(self.fetch_chain_info(chain_id, &validators)).await?;
2199        communicate_with_quorum(
2200            &validators,
2201            &committee,
2202            |_: &()| (),
2203            |remote_node| async move {
2204                self.synchronize_chain_state_from(&remote_node, chain_id)
2205                    .await
2206            },
2207            self.options.quorum_grace_period,
2208        )
2209        .await?;
2210
2211        self.local_node
2212            .chain_info(chain_id)
2213            .await
2214            .map_err(Into::into)
2215    }
2216
2217    /// Downloads any certificates from the specified validator that we are missing for the given
2218    /// chain.
2219    ///
2220    /// If the chain is not in follow-only mode, also fetches and processes manager values
2221    /// (timeout certificates, proposals, locking blocks) for consensus participation.
2222    #[instrument(level = "trace", skip(self, remote_node, chain_id))]
2223    pub(crate) async fn synchronize_chain_state_from(
2224        &self,
2225        remote_node: &RemoteNode<Env::ValidatorNode>,
2226        chain_id: ChainId,
2227    ) -> Result<(), chain_client::Error> {
2228        let with_manager_values = !self.is_chain_follow_only(chain_id);
2229        let query = if with_manager_values {
2230            ChainInfoQuery::new(chain_id).with_manager_values()
2231        } else {
2232            ChainInfoQuery::new(chain_id)
2233        };
2234        let query = query.with_latest_checkpoint_height();
2235        let remote_info = remote_node.handle_chain_info_query(query).await?;
2236
2237        // If the validator advertises a checkpoint and our local tip is below it, fetch
2238        // the checkpoint cert and blob and apply the cert directly. The chain worker's
2239        // `process_confirmed_block` recognises the gap-plus-checkpoint case and installs
2240        // the chain's execution state from the blob before re-executing the cert. The
2241        // subsequent `download_certificates_from` call then resumes from the post-
2242        // checkpoint height and skips downloading any pre-checkpoint blocks.
2243        if let Some(checkpoint_height) = remote_info.requested_latest_checkpoint_height {
2244            self.bootstrap_chain_from_checkpoint(remote_node, chain_id, checkpoint_height)
2245                .await?;
2246        }
2247
2248        let local_info = self
2249            .download_certificates_from(remote_node, chain_id, remote_info.next_block_height, None)
2250            .await?;
2251
2252        if !with_manager_values {
2253            return Ok(());
2254        }
2255
2256        // If we are at the same height as the remote node, we also update our chain manager.
2257        let local_height = local_info.next_block_height;
2258        if local_height != remote_info.next_block_height {
2259            debug!(
2260                remote_node = remote_node.address(),
2261                remote_height = %remote_info.next_block_height,
2262                local_height = %local_height,
2263                "synced from validator, but remote height and local height are different",
2264            );
2265            return Ok(());
2266        };
2267
2268        if let Some(timeout) = remote_info.manager.timeout {
2269            self.handle_certificate::<Timeout>(*timeout).await?;
2270        }
2271        let mut proposals = Vec::new();
2272        if let Some(proposal) = remote_info.manager.requested_signed_proposal {
2273            proposals.push(*proposal);
2274        }
2275        if let Some(proposal) = remote_info.manager.requested_proposed {
2276            proposals.push(*proposal);
2277        }
2278        if let Some(locking) = remote_info.manager.requested_locking {
2279            match *locking {
2280                LockingBlock::Fast(proposal) => {
2281                    proposals.push(proposal);
2282                }
2283                LockingBlock::Regular(cert) => {
2284                    let hash = cert.hash();
2285                    if let Err(error) = self.try_process_locking_block_from(remote_node, cert).await
2286                    {
2287                        debug!(
2288                            remote_node = remote_node.address(),
2289                            %hash,
2290                            height = %local_height,
2291                            %error,
2292                            "skipping locked block from validator",
2293                        );
2294                    }
2295                }
2296            }
2297        }
2298        'proposal_loop: for proposal in proposals {
2299            let owner: AccountOwner = proposal.owner();
2300            if let Err(mut err) = self
2301                .local_node
2302                .handle_block_proposal(proposal.clone())
2303                .await
2304            {
2305                if let LocalNodeError::BlobsNotFound(_) = &err {
2306                    let required_blob_ids = proposal.required_blob_ids().collect::<Vec<_>>();
2307                    if !required_blob_ids.is_empty() {
2308                        let mut blobs = Vec::new();
2309                        for blob_id in required_blob_ids {
2310                            let blob_content = match self
2311                                .requests_scheduler
2312                                .download_pending_blob(remote_node, chain_id, blob_id)
2313                                .await
2314                            {
2315                                Ok(content) => content,
2316                                Err(error) => {
2317                                    info!(
2318                                        remote_node = remote_node.address(),
2319                                        height = %local_height,
2320                                        proposer = %owner,
2321                                        %blob_id,
2322                                        %error,
2323                                        "skipping proposal from validator; failed to download blob",
2324                                    );
2325                                    continue 'proposal_loop;
2326                                }
2327                            };
2328                            blobs.push(Blob::new(blob_content));
2329                        }
2330                        self.local_node
2331                            .handle_pending_blobs(chain_id, blobs)
2332                            .await?;
2333                        // We found the missing blobs: retry.
2334                        if let Err(new_err) = self
2335                            .local_node
2336                            .handle_block_proposal(proposal.clone())
2337                            .await
2338                        {
2339                            err = new_err;
2340                        } else {
2341                            continue;
2342                        }
2343                    }
2344                    if let LocalNodeError::BlobsNotFound(blob_ids) = &err {
2345                        self.update_local_node_with_blobs_from(
2346                            blob_ids.clone(),
2347                            slice::from_ref(remote_node),
2348                        )
2349                        .await?;
2350                        // We found the missing blobs: retry.
2351                        if let Err(new_err) = self
2352                            .local_node
2353                            .handle_block_proposal(proposal.clone())
2354                            .await
2355                        {
2356                            err = new_err;
2357                        } else {
2358                            continue;
2359                        }
2360                    }
2361                }
2362                if let LocalNodeError::EventsNotFound(event_ids) = &err {
2363                    if let Err(error) =
2364                        Box::pin(self.download_certificates_for_events(event_ids)).await
2365                    {
2366                        info!(
2367                            remote_node = remote_node.address(),
2368                            height = %local_height,
2369                            proposer = %owner,
2370                            %error,
2371                            "skipping proposal from validator; failed to download events",
2372                        );
2373                        continue 'proposal_loop;
2374                    }
2375                    // We found the missing publisher chain data: retry.
2376                    if let Err(new_err) = self
2377                        .local_node
2378                        .handle_block_proposal(proposal.clone())
2379                        .await
2380                    {
2381                        err = new_err;
2382                    } else {
2383                        continue;
2384                    }
2385                }
2386                // The local node reports every missing sender bundle in a single
2387                // `MissingCrossChainUpdates`, so we download them all in one pass and retry once.
2388                if let LocalNodeError::WorkerError(WorkerError::ChainError(chain_err)) = &err {
2389                    if let ChainError::MissingCrossChainUpdates { chain_id, bundles } = &**chain_err
2390                    {
2391                        let chain_id = *chain_id;
2392                        // `download_sender_block_with_sending_ancestors` walks each origin's
2393                        // message-bearing blocks back from the given height, so the highest missing
2394                        // height per origin subsumes the lower ones. Deduplicate to that (also
2395                        // ending the borrow of `err` so we can reassign it below), then download the
2396                        // independent origins concurrently, bounded by `max_joined_tasks`.
2397                        let mut origin_heights: BTreeMap<ChainId, BlockHeight> = BTreeMap::new();
2398                        for (origin, height) in bundles {
2399                            let entry = origin_heights.entry(*origin).or_insert(*height);
2400                            *entry = (*entry).max(*height);
2401                        }
2402                        stream::iter(origin_heights.into_iter().map(|(origin, height)| {
2403                            self.download_sender_block_with_sending_ancestors(
2404                                chain_id,
2405                                origin,
2406                                height,
2407                                remote_node,
2408                            )
2409                        }))
2410                        .buffer_unordered(self.options.max_joined_tasks)
2411                        .collect::<Vec<_>>()
2412                        .await
2413                        .into_iter()
2414                        .collect::<Result<(), _>>()?;
2415                        if let Err(new_err) = self
2416                            .local_node
2417                            .handle_block_proposal(proposal.clone())
2418                            .await
2419                        {
2420                            err = new_err;
2421                        } else {
2422                            continue 'proposal_loop;
2423                        }
2424                    }
2425                }
2426
2427                debug!(
2428                    remote_node = remote_node.address(),
2429                    proposer = %owner,
2430                    height = %local_height,
2431                    error = %err,
2432                    "skipping proposal from validator",
2433                );
2434            }
2435        }
2436        Ok(())
2437    }
2438
2439    async fn try_process_locking_block_from(
2440        &self,
2441        remote_node: &RemoteNode<Env::ValidatorNode>,
2442        certificate: ValidatedBlockCertificate,
2443    ) -> Result<(), chain_client::Error> {
2444        let chain_id = certificate.inner().chain_id();
2445        let mut downloaded_blobs = HashSet::<BlobId>::new();
2446        let mut events = EventSetDownloader::new(self);
2447        loop {
2448            let result = self
2449                .handle_certificate::<ValidatedBlock>(certificate.clone())
2450                .await;
2451            if let Err(LocalNodeError::BlobsNotFound(blob_ids)) = &result {
2452                let new_blobs = filter_new(blob_ids, &downloaded_blobs);
2453                if !new_blobs.is_empty() {
2454                    let mut blobs = Vec::new();
2455                    for blob_id in &new_blobs {
2456                        let blob_content = self
2457                            .requests_scheduler
2458                            .download_pending_blob(remote_node, chain_id, *blob_id)
2459                            .await?;
2460                        blobs.push(Blob::new(blob_content));
2461                    }
2462                    self.local_node
2463                        .handle_pending_blobs(chain_id, blobs)
2464                        .await?;
2465                    downloaded_blobs.extend(new_blobs);
2466                    continue;
2467                }
2468            }
2469            if let Err(LocalNodeError::EventsNotFound(event_ids)) = &result {
2470                if events.download_new(event_ids).await? {
2471                    continue;
2472                }
2473            }
2474            result?;
2475            return Ok(());
2476        }
2477    }
2478
2479    /// Downloads and processes from the specified validators a confirmed block certificates that
2480    /// use the given blobs. If this succeeds, the blob will be in our storage.
2481    async fn update_local_node_with_blobs_from(
2482        &self,
2483        blob_ids: Vec<BlobId>,
2484        remote_nodes: &[RemoteNode<Env::ValidatorNode>],
2485    ) -> Result<Vec<CacheArc<Blob>>, chain_client::Error> {
2486        let hedge_delay = self.options.blob_download_hedge_delay;
2487        // Deduplicate IDs.
2488        let blob_ids = blob_ids.into_iter().collect::<BTreeSet<_>>();
2489        stream::iter(blob_ids.into_iter().map(|blob_id| {
2490            communicate_concurrently(
2491                remote_nodes,
2492                async move |remote_node| {
2493                    let certificate = self
2494                        .requests_scheduler
2495                        .download_certificate_for_blob(&remote_node, blob_id)
2496                        .await?;
2497                    self.receive_sender_certificate(
2498                        self.storage_client().cache_certificate(certificate),
2499                        ReceiveCertificateMode::NeedsCheck,
2500                        Some(vec![remote_node.clone()]),
2501                    )
2502                    .await?;
2503                    let blob = self
2504                        .local_node
2505                        .storage_client()
2506                        .read_blob(blob_id)
2507                        .await?
2508                        .ok_or_else(|| LocalNodeError::BlobsNotFound(vec![blob_id]))?;
2509                    Result::<_, chain_client::Error>::Ok(blob)
2510                },
2511                hedge_delay,
2512                self.storage_client().clock(),
2513            )
2514            .map_err(move |errors| {
2515                for (validator, error) in &errors {
2516                    warn!(
2517                        %validator,
2518                        %blob_id,
2519                        %error,
2520                        "failed to download certificate-for-blob from validator",
2521                    );
2522                }
2523                chain_client::Error::CannotDownloadBlob(blob_id)
2524            })
2525        }))
2526        .buffer_unordered(self.options.max_joined_tasks)
2527        .collect::<Vec<_>>()
2528        .await
2529        .into_iter()
2530        .collect()
2531    }
2532
2533    /// Attempts to execute the block locally. If any incoming message execution fails, that
2534    /// message is rejected and execution is retried, until the block accepts only messages
2535    /// that succeed.
2536    ///
2537    /// Attempts to execute the block locally with a specified policy for handling bundle failures.
2538    /// If any attempt to read a blob fails, the blob is downloaded and execution is retried.
2539    ///
2540    /// Returns the modified block (bundles may be rejected/removed based on the policy)
2541    /// and the execution result.
2542    #[instrument(level = "trace", skip(self, block))]
2543    async fn stage_block_execution(
2544        &self,
2545        block: ProposedBlock,
2546        round: Option<u32>,
2547        published_blobs: Vec<Blob>,
2548        policy: BundleExecutionPolicy,
2549    ) -> Result<(Block, ChainInfoResponse, HashSet<ChainId>), chain_client::Error> {
2550        let mut events = EventSetDownloader::new(self);
2551        loop {
2552            let result = self
2553                .local_node
2554                .stage_block_execution(
2555                    block.clone(),
2556                    round,
2557                    published_blobs.clone(),
2558                    policy.clone(),
2559                )
2560                .await;
2561            if let Err(LocalNodeError::BlobsNotFound(blob_ids)) = &result {
2562                let validators = self.validator_nodes().await?;
2563                self.update_local_node_with_blobs_from(blob_ids.clone(), &validators)
2564                    .await?;
2565                continue; // We found the missing blob: retry.
2566            }
2567            if let Err(LocalNodeError::EventsNotFound(event_ids)) = &result {
2568                if events.download_new(event_ids).await? {
2569                    continue; // We downloaded new publisher chain data: retry.
2570                }
2571                // All reported events were already downloaded; don't loop forever.
2572            }
2573            if let Ok((_, executed_block, _, _, _)) = &result {
2574                let hash = executed_block.hash();
2575                let notification = Notification {
2576                    chain_id: executed_block.header.chain_id,
2577                    reason: Reason::BlockExecuted {
2578                        height: executed_block.header.height,
2579                        hash,
2580                    },
2581                };
2582                self.notifier.notify(&[notification]);
2583            }
2584            let (
2585                _modified_block,
2586                executed_block,
2587                response,
2588                _resource_tracker,
2589                never_reject_origins,
2590            ) = result?;
2591            return Ok((executed_block, response, never_reject_origins));
2592        }
2593    }
2594}
2595
2596/// Returns the items in `ids` that are not yet present in `already_downloaded`.
2597fn filter_new<T: Clone + Eq + std::hash::Hash>(
2598    ids: &[T],
2599    already_downloaded: &HashSet<T>,
2600) -> Vec<T> {
2601    ids.iter()
2602        .filter(|id| !already_downloaded.contains(*id))
2603        .cloned()
2604        .collect()
2605}
2606
2607/// Per-call deduplication for an event-download retry loop. Holds the set of
2608/// events the loop has already downloaded and the [`Client`] used to fetch new
2609/// ones — call `download_new` each time the inner operation reports
2610/// `EventsNotFound` and continue the loop only if it returns `true`.
2611pub(crate) struct EventSetDownloader<'a, Env: Environment> {
2612    client: &'a Client<Env>,
2613    downloaded: HashSet<EventId>,
2614}
2615
2616impl<'a, Env: Environment> EventSetDownloader<'a, Env> {
2617    pub(crate) fn new(client: &'a Client<Env>) -> Self {
2618        Self {
2619            client,
2620            downloaded: HashSet::new(),
2621        }
2622    }
2623
2624    /// If any of `event_ids` haven't been downloaded yet, fetches the publisher
2625    /// certificates that contain them and returns `true`. Returns `false`
2626    /// (without downloading) if every reported event has already been
2627    /// downloaded — that prevents an infinite retry loop when the events are
2628    /// genuinely unavailable.
2629    pub(crate) async fn download_new(
2630        &mut self,
2631        event_ids: &[EventId],
2632    ) -> Result<bool, chain_client::Error> {
2633        let new_events = filter_new(event_ids, &self.downloaded);
2634        if new_events.is_empty() {
2635            return Ok(false);
2636        }
2637        Box::pin(self.client.download_certificates_for_events(&new_events)).await?;
2638        self.downloaded.extend(new_events);
2639        Ok(true)
2640    }
2641}
2642
2643/// The clock backing the environment's storage.
2644///
2645/// All client-side coordination logic (retries, backoff, request TTLs, the notification
2646/// circuit breaker) reads time through this clock rather than the wall clock, so it can be
2647/// driven deterministically by a simulated clock (e.g. `TestClock`) in tests.
2648pub(crate) type ClockOf<Env> = <<Env as Environment>::Storage as linera_storage::Storage>::Clock;
2649
2650/// Races `operation` across peers with a hedged, **failure-responsive** fan-out, returning the
2651/// first `Ok` (or every error if all attempts fail).
2652///
2653/// `first_peer` is tried immediately. `next_peer` then supplies additional peers, each started
2654/// either *immediately* when an in-flight attempt fails (no point waiting once we know an attempt
2655/// failed), or after the in-flight attempt has run for `hedge_schedule(started)` without answering
2656/// — hedging against a slow peer by racing an extra request; the slow attempt is **not** cancelled.
2657/// `hedge_schedule` maps the number of attempts started so far to the delay before starting the
2658/// next one (e.g. `|k| delay * k` for a linearly-growing stagger). All sleeping is on `clock`, so
2659/// this runs in (possibly simulated) time; freezing the clock disables the slow-peer hedge while
2660/// still advancing on every failure.
2661pub(crate) async fn hedged_fan_out<Peer, T, Err, NextPeer, NextFut, Op, OpFut>(
2662    first_peer: Peer,
2663    mut next_peer: NextPeer,
2664    operation: Op,
2665    hedge_schedule: impl Fn(usize) -> Duration,
2666    // `Sync` so `clock.sleep_for(..)` yields a `Send` future, as required when this fan-out is
2667    // spawned (e.g. background certificate downloads). All storage clocks are `Sync`.
2668    clock: &(impl linera_storage::Clock + Sync),
2669) -> Result<T, Vec<Err>>
2670where
2671    NextPeer: FnMut() -> NextFut,
2672    NextFut: Future<Output = Option<Peer>>,
2673    Op: Fn(Peer) -> OpFut,
2674    OpFut: Future<Output = Result<T, Err>>,
2675{
2676    use futures::future::{select, Either};
2677
2678    let mut in_flight = FuturesUnordered::new();
2679    let mut errors = vec![];
2680    let mut started = 0usize;
2681    let arm = |started: usize| clock.sleep_for(hedge_schedule(started));
2682
2683    in_flight.push(operation(first_peer));
2684    started += 1;
2685    let mut hedge = arm(started);
2686
2687    loop {
2688        if in_flight.is_empty() {
2689            // Nothing running: start the next peer, or stop if there are none left.
2690            match next_peer().await {
2691                Some(peer) => {
2692                    in_flight.push(operation(peer));
2693                    started += 1;
2694                    hedge = arm(started);
2695                }
2696                None => return Err(errors),
2697            }
2698            continue;
2699        }
2700        match select(in_flight.next(), hedge).await {
2701            // An attempt succeeded.
2702            Either::Left((Some(Ok(value)), _)) => return Ok(value),
2703            // An attempt failed: try the next peer right away rather than waiting out the hedge.
2704            Either::Left((Some(Err(error)), pending_hedge)) => {
2705                errors.push(error);
2706                hedge = pending_hedge;
2707                if let Some(peer) = next_peer().await {
2708                    in_flight.push(operation(peer));
2709                    started += 1;
2710                    hedge = arm(started);
2711                }
2712            }
2713            // All in-flight attempts drained; the loop top decides what to do next.
2714            Either::Left((None, pending_hedge)) => hedge = pending_hedge,
2715            // An attempt is slow: hedge by starting another peer, if any remain.
2716            Either::Right(((), _)) => match next_peer().await {
2717                Some(peer) => {
2718                    in_flight.push(operation(peer));
2719                    started += 1;
2720                    hedge = arm(started);
2721                }
2722                None => break,
2723            },
2724        }
2725    }
2726
2727    // No more peers to start; just wait for the remaining in-flight attempts.
2728    while let Some(result) = in_flight.next().await {
2729        match result {
2730            Ok(value) => return Ok(value),
2731            Err(error) => errors.push(error),
2732        }
2733    }
2734    Err(errors)
2735}
2736
2737/// Performs `f` on the validators with a hedged, staggered fan-out (see [`hedged_fan_out`]),
2738/// returning the first `Ok` result, or every `(validator, error)` pair if all of them fail.
2739///
2740/// The hedge before starting the n-th validator grows quadratically with `n`, so we stay
2741/// reluctant to fan out to many validators at once when one is merely slow.
2742async fn communicate_concurrently<A, E, F, R, V>(
2743    nodes: &[RemoteNode<A>],
2744    f: F,
2745    hedge_delay: Duration,
2746    clock: &(impl linera_storage::Clock + Sync),
2747) -> Result<V, Vec<(ValidatorPublicKey, E)>>
2748where
2749    F: Clone + FnOnce(RemoteNode<A>) -> R,
2750    RemoteNode<A>: Clone,
2751    R: Future<Output = Result<V, E>>,
2752{
2753    let mut nodes = nodes.to_vec();
2754    nodes.shuffle(&mut rand::thread_rng());
2755    let mut nodes = nodes.into_iter();
2756    let Some(first_peer) = nodes.next() else {
2757        return Err(vec![]);
2758    };
2759    hedged_fan_out(
2760        first_peer,
2761        move || std::future::ready(nodes.next()),
2762        |node: RemoteNode<A>| {
2763            let fun = f.clone();
2764            async move {
2765                let public_key = node.public_key;
2766                fun(node).await.map_err(|err| (public_key, err))
2767            }
2768        },
2769        |started| {
2770            let k = u32::try_from(started).unwrap_or(u32::MAX);
2771            hedge_delay.saturating_mul(k).saturating_mul(k)
2772        },
2773        clock,
2774    )
2775    .await
2776}
2777
2778/// Wrapper for `AbortHandle` that aborts when its dropped.
2779#[must_use]
2780pub struct AbortOnDrop(pub AbortHandle);
2781
2782impl Drop for AbortOnDrop {
2783    #[instrument(level = "trace", skip(self))]
2784    fn drop(&mut self) {
2785        self.0.abort();
2786    }
2787}
2788
2789/// A pending proposed block, together with its published blobs.
2790#[derive(Clone, Serialize, Deserialize)]
2791pub struct PendingProposal {
2792    /// The proposed block.
2793    pub block: ProposedBlock,
2794    /// The blobs published by the proposed block.
2795    pub blobs: Vec<Blob>,
2796    /// The execution outcome from the AutoRetry execution, used as a sanity check
2797    /// against the committed execution outcome.
2798    #[serde(default)]
2799    pub auto_retry_outcome: Option<BlockExecutionOutcome>,
2800    /// The round in which this proposal was first submitted, if any.
2801    #[serde(default)]
2802    pub round: Option<Round>,
2803}
2804
2805/// A validator's report, collected during a quorum round, that it is ahead of the local node
2806/// on a chain (a [`chain_client::Error::LocalNodeLagging`] signal from the updater).
2807struct LagReport<N> {
2808    remote_node: RemoteNode<N>,
2809    chain_id: ChainId,
2810    error: NodeError,
2811}
2812
2813impl<N> LagReport<N> {
2814    /// The height and round the validator reported being at, as far as the error reveals them.
2815    /// Used to pull from the most advanced validators first.
2816    fn remote_progress(&self) -> (Option<BlockHeight>, Option<Round>) {
2817        match &self.error {
2818            NodeError::UnexpectedBlockHeight {
2819                expected_block_height,
2820                ..
2821            } => (Some(*expected_block_height), None),
2822            NodeError::WrongRound(round) => (None, Some(*round)),
2823            _ => (None, None),
2824        }
2825    }
2826}
2827
2828enum ReceiveCertificateMode {
2829    NeedsCheck,
2830    AlreadyChecked,
2831}
2832
2833enum CheckCertificateResult {
2834    /// The certificate's epoch has been revoked on the admin chain.
2835    OldEpoch,
2836    /// The committee for the certificate's epoch is unknown to us yet, e.g. because
2837    /// our local view of the admin chain is behind.
2838    FutureEpoch,
2839    New,
2840}
2841
2842impl CheckCertificateResult {
2843    fn into_result(self) -> Result<(), chain_client::Error> {
2844        match self {
2845            Self::OldEpoch => Err(chain_client::Error::CommitteeDeprecationError),
2846            Self::FutureEpoch => Err(chain_client::Error::CommitteeSynchronizationError),
2847            Self::New => Ok(()),
2848        }
2849    }
2850}
2851
2852/// Creates a compressed Contract, Service and bytecode, plus an optional
2853/// `ApplicationFormats` blob built from the BCS-encoded `Formats` description
2854/// bytes.
2855#[cfg(not(target_arch = "wasm32"))]
2856pub async fn create_bytecode_blobs(
2857    contract: Bytecode,
2858    service: Bytecode,
2859    vm_runtime: VmRuntime,
2860    formats: Option<Vec<u8>>,
2861) -> (Vec<Blob>, ModuleId) {
2862    let formats_blob = formats.map(Blob::new_application_formats);
2863    let formats_blob_hash = formats_blob.as_ref().map(|blob| blob.id().hash);
2864    let (mut blobs, module_id) = match vm_runtime {
2865        VmRuntime::Wasm => {
2866            let (compressed_contract, compressed_service) =
2867                tokio::task::spawn_blocking(move || (contract.compress(), service.compress()))
2868                    .await
2869                    .expect("Compression should not panic");
2870            let contract_blob = Blob::new_contract_bytecode(compressed_contract);
2871            let service_blob = Blob::new_service_bytecode(compressed_service);
2872            let module_id = ModuleId::new_with_formats(
2873                contract_blob.id().hash,
2874                service_blob.id().hash,
2875                vm_runtime,
2876                formats_blob_hash,
2877            );
2878            (vec![contract_blob, service_blob], module_id)
2879        }
2880        VmRuntime::Evm => {
2881            let compressed_contract = contract.compress();
2882            let evm_contract_blob = Blob::new_evm_bytecode(compressed_contract);
2883            let module_id = ModuleId::new_with_formats(
2884                evm_contract_blob.id().hash,
2885                evm_contract_blob.id().hash,
2886                vm_runtime,
2887                formats_blob_hash,
2888            );
2889            (vec![evm_contract_blob], module_id)
2890        }
2891    };
2892    if let Some(blob) = formats_blob {
2893        blobs.push(blob);
2894    }
2895    (blobs, module_id)
2896}
2897
2898#[cfg(test)]
2899mod chain_modes_tests {
2900    use std::collections::BTreeSet;
2901
2902    use linera_base::{crypto::CryptoHash, identifiers::ChainId};
2903
2904    use super::{ChainModes, ListeningMode};
2905
2906    /// `remove_mode` reports the previous mode (and `None` for an absent chain), and only rehashes
2907    /// the memoized fully-tracked set when the removed chain was `FullChain` — removing an
2908    /// `EventsOnly` chain leaves that set untouched.
2909    #[test]
2910    fn remove_mode_updates_full_set_only_for_full_chains() {
2911        let mut modes = ChainModes::default();
2912        let full = ChainId(CryptoHash::test_hash("full"));
2913        let events_only = ChainId(CryptoHash::test_hash("events-only"));
2914        modes.extend_mode(full, ListeningMode::FullChain);
2915        modes.extend_mode(events_only, ListeningMode::EventsOnly(BTreeSet::new()));
2916
2917        let full_hash_before = modes.full().hash();
2918        // Removing the events-only chain returns its mode but doesn't touch the fully-tracked set.
2919        assert!(matches!(
2920            modes.remove_mode(&events_only),
2921            Some(ListeningMode::EventsOnly(_))
2922        ));
2923        assert!(modes.get(&events_only).is_none());
2924        // Removing it again is a no-op.
2925        assert!(modes.remove_mode(&events_only).is_none());
2926        assert_eq!(modes.full().hash(), full_hash_before);
2927        assert_eq!(modes.full().inner().0, BTreeSet::from([full]));
2928
2929        // Removing the full chain empties and rehashes the fully-tracked set.
2930        assert_eq!(modes.remove_mode(&full), Some(ListeningMode::FullChain));
2931        assert_ne!(modes.full().hash(), full_hash_before);
2932        assert!(modes.full().inner().0.is_empty());
2933    }
2934}
2935
2936#[cfg(test)]
2937mod communicate_concurrently_tests {
2938    use std::sync::{
2939        atomic::{AtomicUsize, Ordering},
2940        Arc,
2941    };
2942
2943    use linera_base::crypto::ValidatorKeypair;
2944    use linera_storage::TestClock;
2945
2946    use super::*;
2947
2948    fn test_node() -> RemoteNode<()> {
2949        RemoteNode {
2950            public_key: ValidatorKeypair::generate().public_key,
2951            node: (),
2952        }
2953    }
2954
2955    /// When every node fails, all errors are collected and we return promptly — crucially without
2956    /// ever waiting out the hedge delay, so the frozen clock never advances.
2957    #[tokio::test]
2958    async fn does_not_wait_after_failures() {
2959        let clock = TestClock::new();
2960        let nodes: Vec<_> = (0..5).map(|_| test_node()).collect();
2961        let calls = Arc::new(AtomicUsize::new(0));
2962        let result: Result<(), Vec<(ValidatorPublicKey, &str)>> = communicate_concurrently(
2963            &nodes,
2964            {
2965                let calls = calls.clone();
2966                move |_node| {
2967                    let calls = calls.clone();
2968                    async move {
2969                        calls.fetch_add(1, Ordering::SeqCst);
2970                        Err("unavailable")
2971                    }
2972                }
2973            },
2974            Duration::from_secs(30),
2975            &clock,
2976        )
2977        .await;
2978        assert_eq!(result.unwrap_err().len(), 5);
2979        assert_eq!(calls.load(Ordering::SeqCst), 5);
2980        // The hedge timer was never needed (every node failed), so virtual time did not advance.
2981        assert_eq!(clock.current_time(), Timestamp::from(0));
2982    }
2983
2984    /// A single working node is reached by failing over the others — again without advancing the
2985    /// clock, regardless of where the shuffle places it.
2986    #[tokio::test]
2987    async fn fails_over_to_a_working_node() {
2988        let clock = TestClock::new();
2989        let nodes: Vec<_> = (0..5).map(|_| test_node()).collect();
2990        let working = nodes[3].public_key;
2991        let result: Result<u32, Vec<(ValidatorPublicKey, &str)>> = communicate_concurrently(
2992            &nodes,
2993            move |node| async move {
2994                if node.public_key == working {
2995                    Ok(42)
2996                } else {
2997                    Err("unavailable")
2998                }
2999            },
3000            Duration::from_secs(30),
3001            &clock,
3002        )
3003        .await;
3004        assert_eq!(result.unwrap(), 42);
3005        assert_eq!(clock.current_time(), Timestamp::from(0));
3006    }
3007
3008    // The tests below exercise the `hedged_fan_out` engine directly — peers are modelled as
3009    // `usize`, the operation outcome is chosen per peer, and "slowness" is made controllable with
3010    // notify gates. Driving the (virtual) clock by hand lets us assert the hedging behaviour that
3011    // was impossible to test deterministically on the wall clock.
3012
3013    /// Hands out peers `1..n`; `hedged_fan_out` already holds `first_peer = 0`.
3014    fn peer_source(n: usize) -> impl FnMut() -> std::future::Ready<Option<usize>> {
3015        let mut next = 1usize;
3016        move || {
3017            let peer = (next < n).then_some(next);
3018            next += 1;
3019            std::future::ready(peer)
3020        }
3021    }
3022
3023    /// A slow first peer is hedged by starting the next one, but is **not** cancelled: when it
3024    /// finally answers `Ok`, that answer still wins the race.
3025    #[tokio::test]
3026    async fn slow_first_peer_is_hedged_but_not_cancelled() {
3027        let clock = TestClock::new();
3028        let delay = Duration::from_secs(1);
3029        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
3030        let (started_tx, mut started_rx) = tokio::sync::mpsc::unbounded_channel();
3031        let release0 = Arc::new(tokio::sync::Notify::new());
3032        let gates = [release0.clone(), Arc::new(tokio::sync::Notify::new())];
3033
3034        let operation = {
3035            let order = order.clone();
3036            move |peer: usize| {
3037                let started_tx = started_tx.clone();
3038                let order = order.clone();
3039                let gate = gates[peer].clone();
3040                async move {
3041                    order.lock().unwrap().push(peer);
3042                    started_tx.send(peer).unwrap();
3043                    gate.notified().await;
3044                    if peer == 0 {
3045                        Ok::<u32, &str>(42)
3046                    } else {
3047                        Err("slow loser")
3048                    }
3049                }
3050            }
3051        };
3052
3053        let fan = tokio::spawn({
3054            let clock = clock.clone();
3055            async move {
3056                hedged_fan_out(
3057                    0usize,
3058                    peer_source(2),
3059                    operation,
3060                    move |k| delay * u32::try_from(k).unwrap_or(u32::MAX),
3061                    &clock,
3062                )
3063                .await
3064            }
3065        });
3066
3067        // Peer 0 starts immediately; the hedge for peer 1 is armed at `delay`.
3068        assert_eq!(started_rx.recv().await, Some(0));
3069        // Firing the hedge starts peer 1 while peer 0 is still in flight.
3070        clock.add(TimeDelta::from_duration(delay));
3071        assert_eq!(started_rx.recv().await, Some(1));
3072        // Peer 0 was not cancelled when the hedge fired: releasing it now still wins the race.
3073        release0.notify_one();
3074        assert_eq!(fan.await.unwrap(), Ok(42));
3075        assert_eq!(*order.lock().unwrap(), vec![0, 1]);
3076    }
3077
3078    /// With every peer hanging, only the (virtual) clock advances the fan-out, and it starts each
3079    /// peer on the cumulative hedge schedule — here the quadratic one `communicate_concurrently`
3080    /// uses.
3081    #[tokio::test]
3082    async fn hedge_schedule_determines_start_times() {
3083        let clock = TestClock::new();
3084        let unit = Duration::from_secs(1);
3085        let starts = Arc::new(std::sync::Mutex::new(Vec::new()));
3086        let (started_tx, mut started_rx) = tokio::sync::mpsc::unbounded_channel();
3087
3088        let operation = {
3089            let starts = starts.clone();
3090            let clock = clock.clone();
3091            move |peer: usize| {
3092                let started_tx = started_tx.clone();
3093                let starts = starts.clone();
3094                let clock = clock.clone();
3095                async move {
3096                    starts.lock().unwrap().push((peer, clock.current_time()));
3097                    started_tx.send(peer).unwrap();
3098                    std::future::pending::<()>().await;
3099                    Ok::<u32, &str>(0)
3100                }
3101            }
3102        };
3103
3104        let fan = tokio::spawn({
3105            let clock = clock.clone();
3106            async move {
3107                hedged_fan_out(
3108                    0usize,
3109                    peer_source(4),
3110                    operation,
3111                    move |k| {
3112                        let k = u32::try_from(k).unwrap_or(u32::MAX);
3113                        unit * k * k
3114                    },
3115                    &clock,
3116                )
3117                .await
3118            }
3119        });
3120
3121        // peer 0 at t=0; hedge(1)=1·unit, hedge(2)=4·unit, hedge(3)=9·unit, applied cumulatively.
3122        assert_eq!(started_rx.recv().await, Some(0));
3123        clock.add(TimeDelta::from_duration(unit));
3124        assert_eq!(started_rx.recv().await, Some(1));
3125        clock.add(TimeDelta::from_duration(unit * 4));
3126        assert_eq!(started_rx.recv().await, Some(2));
3127        clock.add(TimeDelta::from_duration(unit * 9));
3128        assert_eq!(started_rx.recv().await, Some(3));
3129
3130        // Cumulative virtual start times: 0, 1s, 1+4=5s, 5+9=14s (Timestamp is in microseconds).
3131        assert_eq!(
3132            *starts.lock().unwrap(),
3133            vec![
3134                (0, Timestamp::from(0)),
3135                (1, Timestamp::from(1_000_000)),
3136                (2, Timestamp::from(5_000_000)),
3137                (3, Timestamp::from(14_000_000)),
3138            ]
3139        );
3140        fan.abort();
3141    }
3142
3143    /// Freezing the clock disables the slow-peer hedge entirely: a slow first peer never causes a
3144    /// second one to start, yet the fan-out still completes when that peer eventually answers.
3145    #[tokio::test]
3146    async fn frozen_clock_disables_the_hedge() {
3147        let clock = TestClock::new();
3148        let (started_tx, mut started_rx) = tokio::sync::mpsc::unbounded_channel();
3149        let release0 = Arc::new(tokio::sync::Notify::new());
3150
3151        let operation = {
3152            let release0 = release0.clone();
3153            move |peer: usize| {
3154                let started_tx = started_tx.clone();
3155                let release0 = release0.clone();
3156                async move {
3157                    started_tx.send(peer).unwrap();
3158                    if peer == 0 {
3159                        release0.notified().await;
3160                        Ok::<u32, &str>(7)
3161                    } else {
3162                        // If a hedge ever wrongly fired, this peer would win instead.
3163                        Ok(99)
3164                    }
3165                }
3166            }
3167        };
3168
3169        let fan = tokio::spawn({
3170            let clock = clock.clone();
3171            async move {
3172                hedged_fan_out(
3173                    0usize,
3174                    peer_source(2),
3175                    operation,
3176                    move |k| Duration::from_secs(1) * u32::try_from(k).unwrap_or(u32::MAX),
3177                    &clock,
3178                )
3179                .await
3180            }
3181        });
3182
3183        assert_eq!(started_rx.recv().await, Some(0));
3184        // The clock is frozen, so the hedge can never fire: peer 1 must not start.
3185        tokio::task::yield_now().await;
3186        assert!(
3187            started_rx.try_recv().is_err(),
3188            "the hedge must not fire while the clock is frozen"
3189        );
3190        // Peer 0 still wins, without the clock ever advancing.
3191        release0.notify_one();
3192        assert_eq!(fan.await.unwrap(), Ok(7));
3193        assert_eq!(clock.current_time(), Timestamp::from(0));
3194    }
3195
3196    /// On failure the next peer starts immediately rather than waiting out the (here, huge) hedge
3197    /// delay — failover stays responsive even with a hedge already armed.
3198    #[tokio::test]
3199    async fn failure_fails_over_without_waiting_out_the_hedge() {
3200        let clock = TestClock::new();
3201        let (started_tx, mut started_rx) = tokio::sync::mpsc::unbounded_channel();
3202        let release0 = Arc::new(tokio::sync::Notify::new());
3203
3204        let operation = {
3205            let release0 = release0.clone();
3206            move |peer: usize| {
3207                let started_tx = started_tx.clone();
3208                let release0 = release0.clone();
3209                async move {
3210                    started_tx.send(peer).unwrap();
3211                    match peer {
3212                        0 => {
3213                            release0.notified().await;
3214                            Err::<u32, &str>("dead")
3215                        }
3216                        1 => Err("dead"),
3217                        _ => Ok(55),
3218                    }
3219                }
3220            }
3221        };
3222
3223        let fan = tokio::spawn({
3224            let clock = clock.clone();
3225            async move {
3226                hedged_fan_out(
3227                    0usize,
3228                    peer_source(3),
3229                    operation,
3230                    // A hedge so large that any waiting would be obvious in virtual time.
3231                    move |k| Duration::from_secs(100) * u32::try_from(k).unwrap_or(u32::MAX),
3232                    &clock,
3233                )
3234                .await
3235            }
3236        });
3237
3238        assert_eq!(started_rx.recv().await, Some(0));
3239        // Peer 0 fails: peer 1 starts at once, then peer 1 fails and peer 2 starts and succeeds.
3240        release0.notify_one();
3241        assert_eq!(started_rx.recv().await, Some(1));
3242        assert_eq!(started_rx.recv().await, Some(2));
3243        assert_eq!(fan.await.unwrap(), Ok(55));
3244        // None of the 100s hedge delays were ever waited out.
3245        assert_eq!(clock.current_time(), Timestamp::from(0));
3246    }
3247}