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