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