Skip to main content

linera_core/client/
mod.rs

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