Skip to main content

linera_core/
worker.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    collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
7    future::Future,
8    pin,
9    sync::{Arc, Mutex, RwLock},
10    time::Duration,
11};
12
13use futures::{
14    future::{self, Either, Shared, WeakShared},
15    FutureExt as _,
16};
17use linera_base::{
18    crypto::{CryptoError, CryptoHash, ValidatorPublicKey},
19    data_types::{
20        ApplicationDescription, ArithmeticError, Blob, BlockHeight, Epoch, Round, TimeDelta,
21        Timestamp,
22    },
23    doc_scalar,
24    identifiers::{AccountOwner, ApplicationId, BlobId, ChainId, EventId, StreamId},
25};
26use linera_cache::{Arc as CacheArc, UniqueValueCache, ValueCache, DEFAULT_CLEANUP_INTERVAL_SECS};
27#[cfg(with_testing)]
28use linera_chain::ChainExecutionContext;
29use linera_chain::{
30    data_types::{BlockProposal, BundleExecutionPolicy, MessageBundle, ProposedBlock},
31    types::{
32        Block, CertificateValue, Certified, ConfirmedBlock, ConfirmedBlockCertificate,
33        GenericCertificate, LiteCertificate, Timeout, TimeoutCertificate, ValidatedBlock,
34        ValidatedBlockCertificate,
35    },
36    ChainError, ChainStateView, StreamCounts,
37};
38use linera_execution::{ExecutionError, ExecutionStateView, Query, QueryOutcome, ResourceTracker};
39use linera_storage::{Clock as _, Storage};
40use linera_views::{context::InactiveContext, ViewError};
41use serde::{Deserialize, Serialize};
42use thiserror::Error;
43use tokio::sync::{mpsc, oneshot, OwnedRwLockReadGuard};
44use tracing::{debug, instrument, trace, warn};
45
46/// A read guard providing access to a chain's [`ChainStateView`].
47///
48/// Holds a read lock on the chain worker state, preventing writes for its
49/// lifetime. The `OwnedRwLockReadGuard` internally holds a strong `Arc`
50/// reference to the `RwLock<ChainWorkerState>`, keeping the state alive.
51/// Dereferences to `ChainStateView`.
52pub struct ChainStateViewReadGuard<S: Storage>(
53    OwnedRwLockReadGuard<ChainWorkerState<S>, ChainStateView<S::Context>>,
54);
55
56impl<S: Storage> std::ops::Deref for ChainStateViewReadGuard<S> {
57    type Target = ChainStateView<S::Context>;
58
59    fn deref(&self) -> &Self::Target {
60        &self.0
61    }
62}
63
64/// Re-export of [`EventSubscriptionsResult`] for use by other crate modules.
65pub(crate) use crate::chain_worker::EventSubscriptionsResult;
66use crate::{
67    chain_worker::{
68        handle, state::ChainWorkerState, BlockOutcome, ChainWorkerConfig, CrossChainUpdateResult,
69        DeliveryNotifier, ProcessConfirmedBlockMode,
70    },
71    client::{ChainModes, ListeningMode},
72    data_types::{ChainInfoQuery, ChainInfoResponse, CrossChainRequest},
73    notifier::Notifier,
74};
75
76/// The default maximum number of confirmed blocks kept in the worker's block cache.
77pub const DEFAULT_BLOCK_CACHE_SIZE: usize = 5_000;
78/// The default maximum number of execution state views kept in the worker's cache.
79pub const DEFAULT_EXECUTION_STATE_CACHE_SIZE: usize = 10_000;
80
81#[cfg(test)]
82#[path = "unit_tests/worker_tests.rs"]
83mod worker_tests;
84
85#[cfg(all(test, feature = "rocksdb"))]
86#[path = "unit_tests/worker_backup_tests.rs"]
87mod worker_backup_tests;
88
89/// Wraps a future in `SyncFuture` on non-web targets so that it satisfies `Sync` bounds.
90/// On web targets the future is returned as-is.
91#[cfg(not(web))]
92pub(crate) fn wrap_future<F: std::future::Future>(f: F) -> sync_wrapper::SyncFuture<F> {
93    sync_wrapper::SyncFuture::new(f)
94}
95
96/// Wraps a future in `SyncFuture` on non-web targets so that it satisfies `Sync` bounds.
97/// On web targets the future is returned as-is.
98#[cfg(web)]
99pub(crate) fn wrap_future<F: std::future::Future>(f: F) -> F {
100    f
101}
102
103#[cfg(with_metrics)]
104mod metrics {
105    use std::sync::LazyLock;
106
107    use linera_base::prometheus_util::{
108        exponential_bucket_interval, register_histogram, register_histogram_vec,
109        register_int_counter, register_int_counter_vec,
110    };
111    use linera_chain::{data_types::MessageAction, types::ConfirmedBlockCertificate};
112    use prometheus::{Histogram, HistogramVec, IntCounter, IntCounterVec};
113
114    pub static NUM_ROUNDS_IN_CERTIFICATE: LazyLock<HistogramVec> = LazyLock::new(|| {
115        register_histogram_vec(
116            "num_rounds_in_certificate",
117            "Number of rounds in certificate",
118            &["certificate_value", "round_type"],
119            exponential_bucket_interval(0.1, 50.0),
120        )
121    });
122
123    pub static NUM_ROUNDS_IN_BLOCK_PROPOSAL: LazyLock<HistogramVec> = LazyLock::new(|| {
124        register_histogram_vec(
125            "num_rounds_in_block_proposal",
126            "Number of rounds in block proposal",
127            &["round_type"],
128            exponential_bucket_interval(0.1, 50.0),
129        )
130    });
131
132    pub static TRANSACTION_COUNT: LazyLock<IntCounterVec> =
133        LazyLock::new(|| register_int_counter_vec("transaction_count", "Transaction count", &[]));
134
135    pub static INCOMING_BUNDLE_COUNT: LazyLock<IntCounter> =
136        LazyLock::new(|| register_int_counter("incoming_bundle_count", "Incoming bundle count"));
137
138    pub static REJECTED_BUNDLE_COUNT: LazyLock<IntCounter> =
139        LazyLock::new(|| register_int_counter("rejected_bundle_count", "Rejected bundle count"));
140
141    pub static INCOMING_MESSAGE_COUNT: LazyLock<IntCounter> =
142        LazyLock::new(|| register_int_counter("incoming_message_count", "Incoming message count"));
143
144    pub static OPERATION_COUNT: LazyLock<IntCounter> =
145        LazyLock::new(|| register_int_counter("operation_count", "Operation count"));
146
147    pub static OPERATIONS_PER_BLOCK: LazyLock<Histogram> = LazyLock::new(|| {
148        register_histogram(
149            "operations_per_block",
150            "Number of operations per block",
151            exponential_bucket_interval(1.0, 10000.0),
152        )
153    });
154
155    pub static INCOMING_BUNDLES_PER_BLOCK: LazyLock<Histogram> = LazyLock::new(|| {
156        register_histogram(
157            "incoming_bundles_per_block",
158            "Number of incoming bundles per block",
159            exponential_bucket_interval(1.0, 10000.0),
160        )
161    });
162
163    pub static TRANSACTIONS_PER_BLOCK: LazyLock<Histogram> = LazyLock::new(|| {
164        register_histogram(
165            "transactions_per_block",
166            "Number of transactions per block",
167            exponential_bucket_interval(1.0, 10000.0),
168        )
169    });
170
171    pub static NUM_BLOCKS: LazyLock<IntCounterVec> = LazyLock::new(|| {
172        register_int_counter_vec("num_blocks", "Number of blocks added to chains", &[])
173    });
174
175    pub static CERTIFICATES_SIGNED: LazyLock<IntCounterVec> = LazyLock::new(|| {
176        register_int_counter_vec(
177            "certificates_signed",
178            "Number of confirmed block certificates signed by each validator",
179            &["validator_name"],
180        )
181    });
182
183    pub static CHAIN_INFO_QUERIES: LazyLock<IntCounter> = LazyLock::new(|| {
184        register_int_counter(
185            "chain_info_queries",
186            "Number of chain info queries processed",
187        )
188    });
189
190    pub static CROSS_CHAIN_BATCH_SIZE: LazyLock<Histogram> = LazyLock::new(|| {
191        register_histogram(
192            "cross_chain_batch_size",
193            "Number of cross-chain requests coalesced into a single per-chain batch",
194            exponential_bucket_interval(1.0, 1000.0),
195        )
196    });
197
198    /// Holds metrics data extracted from a confirmed block certificate.
199    pub struct MetricsData {
200        certificate_log_str: &'static str,
201        round_type: &'static str,
202        round_number: u32,
203        confirmed_transactions: u64,
204        confirmed_incoming_bundles: u64,
205        confirmed_rejected_bundles: u64,
206        confirmed_incoming_messages: u64,
207        confirmed_operations: u64,
208        validators_with_signatures: Vec<String>,
209    }
210
211    impl MetricsData {
212        /// Creates a new `MetricsData` by extracting data from the certificate.
213        pub fn new(certificate: &ConfirmedBlockCertificate) -> Self {
214            Self {
215                certificate_log_str: certificate.inner().to_log_str(),
216                round_type: certificate.round.type_name(),
217                round_number: certificate.round.number(),
218                confirmed_transactions: certificate.block().body.transactions.len() as u64,
219                confirmed_incoming_bundles: certificate.block().body.incoming_bundles().count()
220                    as u64,
221                confirmed_rejected_bundles: certificate
222                    .block()
223                    .body
224                    .incoming_bundles()
225                    .filter(|b| b.action == MessageAction::Reject)
226                    .count() as u64,
227                confirmed_incoming_messages: certificate
228                    .block()
229                    .body
230                    .incoming_bundles()
231                    .map(|b| b.messages().count())
232                    .sum::<usize>() as u64,
233                confirmed_operations: certificate.block().body.operations().count() as u64,
234                validators_with_signatures: certificate
235                    .signatures()
236                    .iter()
237                    .map(|(validator_name, _)| validator_name.to_string())
238                    .collect(),
239            }
240        }
241
242        /// Records the metrics for a processed block.
243        pub fn record(self) {
244            NUM_BLOCKS.with_label_values(&[]).inc();
245            NUM_ROUNDS_IN_CERTIFICATE
246                .with_label_values(&[self.certificate_log_str, self.round_type])
247                .observe(self.round_number as f64);
248            TRANSACTIONS_PER_BLOCK.observe(self.confirmed_transactions as f64);
249            INCOMING_BUNDLES_PER_BLOCK.observe(self.confirmed_incoming_bundles as f64);
250            OPERATIONS_PER_BLOCK.observe(self.confirmed_operations as f64);
251            if self.confirmed_transactions > 0 {
252                TRANSACTION_COUNT
253                    .with_label_values(&[])
254                    .inc_by(self.confirmed_transactions);
255                if self.confirmed_incoming_bundles > 0 {
256                    INCOMING_BUNDLE_COUNT.inc_by(self.confirmed_incoming_bundles);
257                }
258                if self.confirmed_rejected_bundles > 0 {
259                    REJECTED_BUNDLE_COUNT.inc_by(self.confirmed_rejected_bundles);
260                }
261                if self.confirmed_incoming_messages > 0 {
262                    INCOMING_MESSAGE_COUNT.inc_by(self.confirmed_incoming_messages);
263                }
264                if self.confirmed_operations > 0 {
265                    OPERATION_COUNT.inc_by(self.confirmed_operations);
266                }
267            }
268
269            for validator_name in self.validators_with_signatures {
270                CERTIFICATES_SIGNED
271                    .with_label_values(&[&validator_name])
272                    .inc();
273            }
274        }
275    }
276}
277
278/// Instruct the networking layer to send cross-chain requests and/or push notifications.
279#[derive(Default, Debug)]
280pub struct NetworkActions {
281    /// The cross-chain requests
282    pub cross_chain_requests: Vec<CrossChainRequest>,
283    /// The push notifications.
284    pub notifications: Vec<Notification>,
285}
286
287impl NetworkActions {
288    /// Merges the cross-chain requests and notifications from `other` into these actions.
289    pub fn extend(&mut self, other: NetworkActions) {
290        self.cross_chain_requests.extend(other.cross_chain_requests);
291        self.notifications.extend(other.notifications);
292    }
293}
294
295#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
296/// Notification that a chain has a new certified block or a new message.
297#[allow(missing_docs)]
298pub struct Notification {
299    pub chain_id: ChainId,
300    pub reason: Reason,
301}
302
303doc_scalar!(
304    Notification,
305    "Notify that a chain has a new certified block or a new message"
306);
307
308#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
309/// Reason for the notification.
310#[allow(missing_docs)]
311pub enum Reason {
312    NewBlock {
313        height: BlockHeight,
314        hash: CryptoHash,
315    },
316    NewEvents {
317        height: BlockHeight,
318        block_hash: CryptoHash,
319        event_streams: BTreeSet<StreamId>,
320    },
321    NewIncomingBundle {
322        origin: ChainId,
323        height: BlockHeight,
324    },
325    NewRound {
326        height: BlockHeight,
327        round: Round,
328    },
329    BlockExecuted {
330        height: BlockHeight,
331        hash: CryptoHash,
332    },
333}
334
335/// Error type for worker operations.
336#[derive(Debug, Error, strum::IntoStaticStr)]
337#[allow(missing_docs)]
338pub enum WorkerError {
339    #[error(transparent)]
340    CryptoError(#[from] CryptoError),
341
342    #[error(transparent)]
343    ArithmeticError(#[from] ArithmeticError),
344
345    #[error(transparent)]
346    ViewError(#[from] ViewError),
347
348    #[error("Certificates referenced from chain state are missing in storage: {0:?}")]
349    ReadCertificatesError(Vec<CryptoHash>),
350
351    #[error(transparent)]
352    ChainError(#[from] Box<ChainError>),
353
354    #[error(transparent)]
355    BcsError(#[from] bcs::Error),
356
357    // Chain access control
358    #[error("Block was not signed by an authorized owner")]
359    InvalidOwner,
360
361    #[error("Operations in the block are not authenticated by the proper owner: {0}")]
362    InvalidSigner(AccountOwner),
363
364    // Chaining
365    #[error(
366        "Chain is expecting a next block at height {expected_block_height} but the given block \
367        is at height {found_block_height} instead"
368    )]
369    UnexpectedBlockHeight {
370        expected_block_height: BlockHeight,
371        found_block_height: BlockHeight,
372    },
373    #[error("Unexpected epoch {epoch}: chain {chain_id} is at {chain_epoch}")]
374    InvalidEpoch {
375        chain_id: ChainId,
376        chain_epoch: Epoch,
377        epoch: Epoch,
378    },
379
380    #[error("Events not found: {0:?}")]
381    EventsNotFound(Vec<EventId>),
382
383    // Other server-side errors
384    #[error("Invalid cross-chain request")]
385    InvalidCrossChainRequest,
386    #[error("The block does not contain the hash that we expected for the previous block")]
387    InvalidBlockChaining,
388    #[error(
389        "Block timestamp ({block_timestamp}) is further in the future from local time \
390        ({local_time}) than block time grace period ({block_time_grace_period:?})"
391    )]
392    InvalidTimestamp {
393        block_timestamp: Timestamp,
394        local_time: Timestamp,
395        block_time_grace_period: Duration,
396    },
397    #[error("We don't have the value for the certificate.")]
398    MissingCertificateValue,
399    #[error("The hash certificate doesn't match its value.")]
400    InvalidLiteCertificate,
401    #[error("Fast blocks cannot query oracles")]
402    FastBlockUsingOracles,
403    #[error("Blobs not found: {0:?}")]
404    BlobsNotFound(Vec<BlobId>),
405    /// Variant raised when the chain references these block hashes via a
406    /// verified-checkpoint trust mark (`pre_checkpoint_block_trust`) but the
407    /// actual content isn't in storage yet. The caller is expected to upload
408    /// each missing block via `handle_confirmed_certificate`; the trust-mark
409    /// accept path verifies the cert against its own (possibly revoked)
410    /// epoch's committee and writes it through.
411    #[error("Blocks not found: {0:?}")]
412    BlocksNotFound(Vec<CryptoHash>),
413    #[error("Block hash at height {height} for chain {chain_id} not found")]
414    BlockHashNotFound {
415        height: BlockHeight,
416        chain_id: ChainId,
417    },
418    #[error("Block at height {height} on chain {chain_id} not found in local storage")]
419    LocalBlockNotFound {
420        height: BlockHeight,
421        chain_id: ChainId,
422    },
423    #[error("The block proposal is invalid: {0}")]
424    InvalidBlockProposal(String),
425    #[error("Blob was not required by any pending block")]
426    UnexpectedBlob,
427    #[error("Number of published blobs per block must not exceed {0}")]
428    TooManyPublishedBlobs(u64),
429    #[error("Missing network description")]
430    MissingNetworkDescription,
431    #[error("thread error: {0}")]
432    Thread(#[from] web_thread_pool::Error),
433    #[error("Chain worker was poisoned by a journal resolution failure")]
434    PoisonedWorker,
435    #[error("Cross-chain batch was rolled back due to an error in another request")]
436    BatchRolledBack,
437}
438
439impl WorkerError {
440    /// Returns whether this error is caused by an issue in the local node.
441    ///
442    /// Returns `false` whenever the error could be caused by a bad message from a peer.
443    pub fn is_local(&self) -> bool {
444        match self {
445            WorkerError::CryptoError(_)
446            | WorkerError::ArithmeticError(_)
447            | WorkerError::InvalidOwner
448            | WorkerError::InvalidSigner(_)
449            | WorkerError::UnexpectedBlockHeight { .. }
450            | WorkerError::InvalidEpoch { .. }
451            | WorkerError::EventsNotFound(_)
452            | WorkerError::InvalidBlockChaining
453            | WorkerError::InvalidTimestamp { .. }
454            | WorkerError::MissingCertificateValue
455            | WorkerError::InvalidLiteCertificate
456            | WorkerError::FastBlockUsingOracles
457            | WorkerError::BlobsNotFound(_)
458            | WorkerError::BlocksNotFound(_)
459            | WorkerError::InvalidBlockProposal(_)
460            | WorkerError::UnexpectedBlob
461            | WorkerError::TooManyPublishedBlobs(_)
462            | WorkerError::ViewError(ViewError::NotFound(_)) => false,
463            WorkerError::BcsError(_)
464            | WorkerError::InvalidCrossChainRequest
465            | WorkerError::ViewError(_)
466            | WorkerError::BlockHashNotFound { .. }
467            | WorkerError::LocalBlockNotFound { .. }
468            | WorkerError::MissingNetworkDescription
469            | WorkerError::Thread(_)
470            | WorkerError::ReadCertificatesError(_)
471            | WorkerError::PoisonedWorker
472            | WorkerError::BatchRolledBack => true,
473            WorkerError::ChainError(chain_error) => chain_error.is_local(),
474        }
475    }
476
477    /// Returns the qualified error variant name for the `error_type` metric label,
478    /// e.g. `"WorkerError::UnexpectedBlockHeight"`.
479    ///
480    /// For `ChainError` variants, delegates to `ChainError::error_type()` to
481    /// surface the underlying error name rather than just `"ChainError"`.
482    pub fn error_type(&self) -> String {
483        match self {
484            WorkerError::ChainError(chain_error) => chain_error.error_type(),
485            other => {
486                let variant: &'static str = other.into();
487                format!("WorkerError::{variant}")
488            }
489        }
490    }
491
492    /// Returns `true` if this error indicates that the chain worker's in-memory
493    /// state may be inconsistent and must be evicted from the cache.
494    pub(crate) fn must_reload_view(&self) -> bool {
495        matches!(
496            self,
497            WorkerError::PoisonedWorker
498                | WorkerError::ViewError(ViewError::StoreError {
499                    must_reload_view: true,
500                    ..
501                })
502        )
503    }
504
505    /// Returns `true` if this error indicates that the chain's persisted state is
506    /// internally inconsistent, so the worker should consider resetting and
507    /// re-executing it from storage.
508    pub(crate) fn indicates_corrupted_chain_state(&self) -> bool {
509        matches!(
510            self,
511            WorkerError::ChainError(chain_error)
512                if matches!(chain_error.as_ref(), ChainError::CorruptedChainState(_))
513        )
514    }
515}
516
517impl From<ChainError> for WorkerError {
518    #[instrument(level = "trace", skip(chain_error))]
519    fn from(chain_error: ChainError) -> Self {
520        match chain_error {
521            ChainError::ExecutionError(execution_error, context) => match *execution_error {
522                ExecutionError::BlobsNotFound(blob_ids) => Self::BlobsNotFound(blob_ids),
523                ExecutionError::EventsNotFound(event_ids) => Self::EventsNotFound(event_ids),
524                _ => Self::ChainError(Box::new(ChainError::ExecutionError(
525                    execution_error,
526                    context,
527                ))),
528            },
529            error => Self::ChainError(Box::new(error)),
530        }
531    }
532}
533
534#[cfg(with_testing)]
535impl WorkerError {
536    /// Returns the inner [`ExecutionError`] in this error.
537    ///
538    /// # Panics
539    ///
540    /// If this is not caused by an [`ExecutionError`].
541    pub fn expect_execution_error(self, expected_context: ChainExecutionContext) -> ExecutionError {
542        let WorkerError::ChainError(chain_error) = self else {
543            panic!("Expected an `ExecutionError`. Got: {self:#?}");
544        };
545
546        let ChainError::ExecutionError(execution_error, context) = *chain_error else {
547            panic!("Expected an `ExecutionError`. Got: {chain_error:#?}");
548        };
549
550        assert_eq!(context, expected_context);
551
552        *execution_error
553    }
554}
555
556type ChainWorkerArc<S> = Arc<tokio::sync::RwLock<ChainWorkerState<S>>>;
557type ChainWorkerWeak<S> = std::sync::Weak<tokio::sync::RwLock<ChainWorkerState<S>>>;
558type ChainWorkerFuture<S> = Shared<oneshot::Receiver<ChainWorkerWeak<S>>>;
559
560/// Each map entry is a `Shared<oneshot::Receiver<Weak<...>>>`:
561///
562/// - `peek()` returns `None` while a task is loading the worker from storage.
563/// - `peek()` returns `Some(Ok(weak))` once the worker is loaded.
564/// - `peek()` returns `Some(Err(_))` if loading failed (sender dropped).
565///
566/// Callers that find a pending entry clone the `Shared` future and await it.
567type ChainWorkerMap<S> = Arc<papaya::HashMap<ChainId, ChainWorkerFuture<S>>>;
568
569/// A cross-chain request waiting to be processed in a batch.
570pub(crate) enum BatchRequest {
571    Update {
572        origin: ChainId,
573        bundles: Vec<(Epoch, MessageBundle)>,
574        previous_height: Option<BlockHeight>,
575        result_sender: oneshot::Sender<Result<CrossChainUpdateResult, WorkerError>>,
576    },
577    Confirm {
578        recipient: ChainId,
579        latest_height: BlockHeight,
580        result_sender: oneshot::Sender<Result<NetworkActions, WorkerError>>,
581    },
582}
583
584/// The inner future type for cross-chain batch processing.
585///
586/// Wrapped in `Shared<BatchFuture>` so that all tasks waiting for
587/// cross-chain operations on the same chain can cooperatively poll a
588/// single driver. The driver loops: wait for an item from the request channel,
589/// drain the channel, then process all requests in one batch through
590/// [`WorkerState::chain_write`] (one write lock, one save), repeat.
591#[cfg(not(web))]
592type BatchFuture = pin::Pin<Box<dyn Future<Output = ()> + Send>>;
593#[cfg(web)]
594type BatchFuture = pin::Pin<Box<dyn Future<Output = ()>>>;
595
596#[derive(Clone)]
597struct ChainBatchRequestProcessor {
598    /// Sender half of the channel whose receiver lives inside the driver future.
599    sender: mpsc::UnboundedSender<BatchRequest>,
600    /// Weak handle to the shared driver future. Upgraded to `Shared<BatchFuture>`
601    /// by callers who need to poll it.
602    future: WeakShared<BatchFuture>,
603}
604
605impl ChainBatchRequestProcessor {
606    fn create<StorageClient>(
607        worker: WorkerState<StorageClient>,
608        chain_id: ChainId,
609        batch_size_limit: usize,
610    ) -> (ChainBatchRequestProcessor, Shared<BatchFuture>)
611    where
612        StorageClient: Storage + Clone + 'static,
613    {
614        let (sender, mut receiver) = mpsc::unbounded_channel();
615        let future: BatchFuture = Box::pin(async move {
616            while let Some(first) = receiver.recv().await {
617                let mut requests = vec![first];
618                while requests.len() < batch_size_limit {
619                    match receiver.try_recv() {
620                        Ok(request) => requests.push(request),
621                        Err(_) => break,
622                    }
623                }
624                #[cfg(with_metrics)]
625                metrics::CROSS_CHAIN_BATCH_SIZE.observe(requests.len() as f64);
626                // Process the batch through `chain_write` so it inherits the same
627                // cancellation safety (the write and `save` run on a detached task)
628                // and recovery (poisoned-worker eviction / corrupted-state reset) as
629                // every other write path, rather than duplicating that logic here.
630                // `process_batch` reports a result to each request's sender on every
631                // internal path; an `Err` here means `chain_write` failed *before*
632                // running the closure — a worker load error, or an already-poisoned
633                // worker that it has now evicted. In that case the request senders are
634                // dropped, which `enqueue_and_drive` surfaces to callers as
635                // `PoisonedWorker`.
636                if let Err(error) = worker
637                    .chain_write(chain_id, move |mut guard| async move {
638                        guard.process_batch(requests).await
639                    })
640                    .await
641                {
642                    tracing::warn!(%chain_id, %error, "cross-chain batch could not be processed");
643                }
644            }
645        });
646        let shared = future.shared();
647        let weak = shared.downgrade().expect("future has not been polled yet");
648        let batch_processor = ChainBatchRequestProcessor {
649            sender,
650            future: weak,
651        };
652        (batch_processor, shared)
653    }
654}
655
656type ChainBatchMap = Arc<papaya::HashMap<ChainId, ChainBatchRequestProcessor>>;
657
658/// Starts a background task that periodically removes dead weak references
659/// from the chain handle map. The actual lifetime management is handled by
660/// each handle's keep-alive task.
661fn start_sweep<S: Storage + Clone + 'static>(
662    chain_workers: &ChainWorkerMap<S>,
663    config: &ChainWorkerConfig,
664) {
665    // Sweep at the smaller of the two TTLs. If both are None, workers
666    // live forever so there's nothing to sweep.
667    let interval = match (config.ttl, config.sender_chain_ttl) {
668        (None, None) => return,
669        (Some(d), None) | (None, Some(d)) => d,
670        (Some(a), Some(b)) => a.min(b),
671    };
672    let weak_map = Arc::downgrade(chain_workers);
673    linera_base::Task::spawn(async move {
674        loop {
675            linera_base::time::timer::sleep(interval).await;
676            let Some(map) = weak_map.upgrade() else {
677                break;
678            };
679            map.pin_owned().retain(|_, shared| match shared.peek() {
680                Some(Ok(weak)) => weak.strong_count() > 0,
681                Some(Err(_)) => false, // Loading failed; clean up.
682                None => true,          // Still loading; keep.
683            });
684        }
685    })
686    .forget();
687}
688
689/// State of a worker in a validator or a local node.
690pub struct WorkerState<StorageClient: Storage> {
691    /// Access to local persistent storage.
692    storage: StorageClient,
693    /// Configuration options for chain workers.
694    chain_worker_config: ChainWorkerConfig,
695    block_cache: Arc<ValueCache<CryptoHash, ConfirmedBlock>>,
696    execution_state_cache:
697        Option<Arc<UniqueValueCache<CryptoHash, ExecutionStateView<InactiveContext>>>>,
698    /// Chains tracked by a worker, along with their listening modes.
699    pub(crate) chain_modes: Option<Arc<RwLock<ChainModes>>>,
700    /// One-shot channels to notify callers when messages of a particular chain have been
701    /// delivered.
702    delivery_notifiers: Arc<Mutex<DeliveryNotifiers>>,
703    /// The cache of loaded chain workers. Stores weak references; each worker
704    /// manages its own lifetime via a keep-alive task. A background sweep
705    /// periodically removes dead entries.
706    chain_workers: ChainWorkerMap<StorageClient>,
707    /// Per-chain batch processing state for cross-chain requests.
708    chain_batches: ChainBatchMap,
709    /// Shard-routing dispatcher for outbound cross-chain requests. Used when we need
710    /// to send cross-chain requests outside of the normal `NetworkActions` return
711    /// path — in particular, the `RevertConfirm`s emitted after resetting a
712    /// corrupted chain. The RPC server layer installs this; without it, we fall
713    /// back to dispatching locally through `handle_cross_chain_request`.
714    outbound_cross_chain_sender: Option<OutboundCrossChainSender>,
715}
716
717/// Dispatcher for outbound cross-chain requests that handles the source-shard-to-
718/// target-shard routing that the worker itself is not aware of.
719pub type OutboundCrossChainSender = Arc<dyn Fn(CrossChainRequest) + Send + Sync>;
720
721impl<StorageClient> Clone for WorkerState<StorageClient>
722where
723    StorageClient: Storage + Clone,
724{
725    fn clone(&self) -> Self {
726        WorkerState {
727            storage: self.storage.clone(),
728            chain_worker_config: self.chain_worker_config.clone(),
729            block_cache: self.block_cache.clone(),
730            execution_state_cache: self.execution_state_cache.clone(),
731            chain_modes: self.chain_modes.clone(),
732            delivery_notifiers: self.delivery_notifiers.clone(),
733            chain_workers: self.chain_workers.clone(),
734            chain_batches: self.chain_batches.clone(),
735            outbound_cross_chain_sender: self.outbound_cross_chain_sender.clone(),
736        }
737    }
738}
739
740pub(crate) type DeliveryNotifiers = HashMap<ChainId, DeliveryNotifier>;
741
742impl<StorageClient> WorkerState<StorageClient>
743where
744    StorageClient: Storage,
745{
746    /// Returns an instance with the specified cross-chain message chunk limit.
747    #[cfg(with_testing)]
748    #[instrument(level = "trace", skip(self))]
749    pub fn with_cross_chain_message_chunk_limit(mut self, limit: usize) -> Self {
750        self.chain_worker_config.cross_chain_message_chunk_limit = limit;
751        self
752    }
753
754    /// Sets the cross-chain message chunk limit.
755    #[cfg(with_testing)]
756    pub fn set_cross_chain_message_chunk_limit(&mut self, limit: usize) {
757        self.chain_worker_config.cross_chain_message_chunk_limit = limit;
758    }
759
760    /// Returns an instance configured to allow reverting confirmed blocks.
761    #[cfg(with_testing)]
762    #[instrument(level = "trace", skip(self, value))]
763    pub fn with_allow_revert_confirm(mut self, value: bool) -> Self {
764        self.chain_worker_config.allow_revert_confirm = value;
765        self
766    }
767
768    /// Returns the worker's nickname.
769    #[instrument(level = "trace", skip(self))]
770    pub fn nickname(&self) -> &str {
771        &self.chain_worker_config.nickname
772    }
773
774    /// Returns the storage client so that it can be manipulated or queried.
775    #[instrument(level = "trace", skip(self))]
776    #[cfg(not(feature = "test"))]
777    pub(crate) fn storage_client(&self) -> &StorageClient {
778        &self.storage
779    }
780
781    /// Returns the storage client so that it can be manipulated or queried by tests in other
782    /// crates.
783    #[instrument(level = "trace", skip(self))]
784    #[cfg(feature = "test")]
785    pub fn storage_client(&self) -> &StorageClient {
786        &self.storage
787    }
788
789    #[instrument(level = "trace", skip(self, certificate))]
790    pub(crate) async fn full_certificate(
791        &self,
792        certificate: LiteCertificate<'_>,
793    ) -> Result<Either<ConfirmedBlockCertificate, ValidatedBlockCertificate>, WorkerError> {
794        let block = self
795            .block_cache
796            .get(&certificate.value.value_hash)
797            .ok_or(WorkerError::MissingCertificateValue)?;
798        let block = CacheArc::unwrap_or_clone(block);
799
800        match certificate.value.kind {
801            linera_chain::types::CertificateKind::Confirmed => Ok(Either::Left(
802                certificate
803                    .into_confirmed_certificate(block)
804                    .ok_or(WorkerError::InvalidLiteCertificate)?,
805            )),
806            linera_chain::types::CertificateKind::Validated => {
807                let value = ValidatedBlock::from_hashed(block.into_inner());
808                Ok(Either::Right(
809                    certificate
810                        .into_validated_certificate(value)
811                        .ok_or(WorkerError::InvalidLiteCertificate)?,
812                ))
813            }
814            _ => Err(WorkerError::InvalidLiteCertificate),
815        }
816    }
817}
818
819#[allow(async_fn_in_trait)]
820#[cfg_attr(not(web), trait_variant::make(Send))]
821/// A certificate value that the worker knows how to process.
822pub trait ProcessableCertificate: CertificateValue + Sized + 'static {
823    /// The concrete certificate type carrying this value (and, for block values, its
824    /// justification chain).
825    type Certificate: Certified<Value = Self> + Clone + Send + Sync + 'static;
826
827    /// Builds the concrete certificate from a signed quorum and a justification chain. The chain
828    /// is ignored for values (such as `Timeout`) that carry no justification.
829    fn make_certificate(
830        quorum: GenericCertificate<Self>,
831        justification: linera_chain::justification::JustificationChain,
832    ) -> Self::Certificate;
833
834    /// Processes a certificate carrying this value on the given worker.
835    async fn process_certificate<S: Storage + Clone + 'static>(
836        worker: &WorkerState<S>,
837        certificate: Self::Certificate,
838    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError>;
839}
840
841impl ProcessableCertificate for ConfirmedBlock {
842    type Certificate = ConfirmedBlockCertificate;
843
844    fn make_certificate(
845        quorum: GenericCertificate<Self>,
846        justification: linera_chain::justification::JustificationChain,
847    ) -> Self::Certificate {
848        ConfirmedBlockCertificate::from_parts(quorum, justification)
849    }
850
851    async fn process_certificate<S: Storage + Clone + 'static>(
852        worker: &WorkerState<S>,
853        certificate: ConfirmedBlockCertificate,
854    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
855        Box::pin(worker.handle_confirmed_certificate(
856            certificate,
857            ProcessConfirmedBlockMode::Auto,
858            None,
859        ))
860        .await
861    }
862}
863
864impl ProcessableCertificate for ValidatedBlock {
865    type Certificate = ValidatedBlockCertificate;
866
867    fn make_certificate(
868        quorum: GenericCertificate<Self>,
869        justification: linera_chain::justification::JustificationChain,
870    ) -> Self::Certificate {
871        ValidatedBlockCertificate::from_parts(quorum, justification)
872    }
873
874    async fn process_certificate<S: Storage + Clone + 'static>(
875        worker: &WorkerState<S>,
876        certificate: ValidatedBlockCertificate,
877    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
878        Box::pin(worker.handle_validated_certificate(certificate)).await
879    }
880}
881
882impl ProcessableCertificate for Timeout {
883    type Certificate = TimeoutCertificate;
884
885    fn make_certificate(
886        quorum: GenericCertificate<Self>,
887        _justification: linera_chain::justification::JustificationChain,
888    ) -> Self::Certificate {
889        quorum
890    }
891
892    async fn process_certificate<S: Storage + Clone + 'static>(
893        worker: &WorkerState<S>,
894        certificate: TimeoutCertificate,
895    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
896        worker.handle_timeout_certificate(certificate).await
897    }
898}
899
900impl<StorageClient> WorkerState<StorageClient>
901where
902    StorageClient: Storage + Clone + 'static,
903{
904    /// Creates a new `WorkerState`.
905    ///
906    /// The `chain_worker_config` must be fully configured before calling this, because the
907    /// TTL sweep task is started immediately based on the config's TTL values.
908    #[instrument(level = "trace", skip(storage, chain_worker_config))]
909    pub fn new(
910        storage: StorageClient,
911        chain_worker_config: ChainWorkerConfig,
912        chain_modes: Option<Arc<RwLock<ChainModes>>>,
913    ) -> Self {
914        let chain_workers = Arc::new(papaya::HashMap::new());
915        start_sweep(&chain_workers, &chain_worker_config);
916        let block_cache_size = chain_worker_config.block_cache_size;
917        let execution_state_cache_size = chain_worker_config.execution_state_cache_size;
918        WorkerState {
919            storage,
920            chain_worker_config,
921            block_cache: Arc::new(ValueCache::new(
922                "worker_block",
923                block_cache_size,
924                DEFAULT_CLEANUP_INTERVAL_SECS,
925            )),
926            execution_state_cache: (execution_state_cache_size > 0)
927                .then(|| Arc::new(UniqueValueCache::new(execution_state_cache_size))),
928            chain_modes,
929            delivery_notifiers: Arc::default(),
930            chain_workers,
931            // On wasm, `ChainBatchRequestProcessor` is not `Send`/`Sync` (the inner future
932            // isn't `Send`), but `Arc` is still correct: `WorkerState` clones
933            // share this map and wasm is single-threaded.
934            #[cfg_attr(web, expect(clippy::arc_with_non_send_sync))]
935            chain_batches: Arc::new(papaya::HashMap::new()),
936            outbound_cross_chain_sender: None,
937        }
938    }
939
940    /// Installs a shard-routing dispatcher used for outbound cross-chain requests
941    /// generated outside the normal response path (specifically, after resetting a
942    /// corrupted chain). Without it, such requests are dispatched locally in a
943    /// loop via `handle_cross_chain_request`.
944    pub fn with_outbound_cross_chain_sender(mut self, sender: OutboundCrossChainSender) -> Self {
945        self.outbound_cross_chain_sender = Some(sender);
946        self
947    }
948
949    #[instrument(level = "trace", skip(self, certificate, notifier))]
950    #[inline]
951    /// Processes a certificate fully, dispatching any resulting cross-chain requests
952    /// and emitting notifications through the given notifier.
953    pub async fn fully_handle_certificate_with_notifications<C>(
954        &self,
955        certificate: C,
956        notifier: &impl Notifier,
957    ) -> Result<ChainInfoResponse, WorkerError>
958    where
959        C: Certified + Clone + Send + 'static,
960        C::Value: ProcessableCertificate<Certificate = C>,
961    {
962        let notifications = (*notifier).clone();
963        let this = self.clone();
964        linera_base::Task::spawn(async move {
965            let (response, actions) =
966                <C::Value as ProcessableCertificate>::process_certificate(&this, certificate)
967                    .await?;
968            notifications.notify(&actions.notifications);
969            let mut requests = VecDeque::from(actions.cross_chain_requests);
970            while let Some(request) = requests.pop_front() {
971                let actions = this.handle_cross_chain_request(request).await?;
972                requests.extend(actions.cross_chain_requests);
973                notifications.notify(&actions.notifications);
974            }
975            Ok(response)
976        })
977        .await
978    }
979
980    /// Same as [`Self::fully_handle_certificate_with_notifications`] but for a
981    /// confirmed block certificate and with an explicit [`ProcessConfirmedBlockMode`].
982    /// The generic variant always uses [`ProcessConfirmedBlockMode::Auto`].
983    #[instrument(level = "trace", skip(self, certificate, notifier))]
984    #[inline]
985    pub async fn fully_handle_confirmed_certificate_with_notifications(
986        &self,
987        certificate: ConfirmedBlockCertificate,
988        mode: ProcessConfirmedBlockMode,
989        notifier: &impl Notifier,
990    ) -> Result<ChainInfoResponse, WorkerError> {
991        let notifications = (*notifier).clone();
992        let this = self.clone();
993        linera_base::Task::spawn(async move {
994            let (response, actions) =
995                Box::pin(this.handle_confirmed_certificate(certificate, mode, None)).await?;
996            notifications.notify(&actions.notifications);
997            let mut requests = VecDeque::from(actions.cross_chain_requests);
998            while let Some(request) = requests.pop_front() {
999                let actions = this.handle_cross_chain_request(request).await?;
1000                requests.extend(actions.cross_chain_requests);
1001                notifications.notify(&actions.notifications);
1002            }
1003            Ok(response)
1004        })
1005        .await
1006    }
1007
1008    /// Acquires a read lock on the chain worker and executes the given closure.
1009    ///
1010    /// The future is boxed to keep deeply nested types off the stack. On non-web
1011    /// targets it is also wrapped in `SyncFuture` to satisfy `Sync` bounds.
1012    async fn chain_read<R, F, Fut>(&self, chain_id: ChainId, f: F) -> Result<R, WorkerError>
1013    where
1014        F: FnOnce(OwnedRwLockReadGuard<ChainWorkerState<StorageClient>>) -> Fut,
1015        Fut: std::future::Future<Output = Result<R, WorkerError>>,
1016    {
1017        let state = self.get_or_create_chain_worker(chain_id).await?;
1018        let state_ref = &state;
1019        let result = Box::pin(wrap_future(async move {
1020            let guard = handle::read_lock(state_ref).await?;
1021            f(guard).await
1022        }))
1023        .await;
1024        if let Err(error) = &result {
1025            if error.must_reload_view() {
1026                self.evict_poisoned_worker(chain_id, &state);
1027            }
1028        }
1029        result
1030    }
1031
1032    /// Acquires a write lock on the chain worker and executes the given closure.
1033    ///
1034    /// The write work runs on a detached task (via [`linera_base::task::run_detached`])
1035    /// so that caller cancellation does not unwind the task mid-save. The
1036    /// [`RollbackGuard`] lives inside the detached task, so the write lock is held
1037    /// until the DB round-trip and `post_save` have fully completed — subsequent
1038    /// readers, including a freshly-loaded replacement worker, only see the
1039    /// committed state.
1040    ///
1041    /// The outcome inspection and recovery dispatch (poisoned-worker eviction and
1042    /// corrupted-state reset) also run *inside* the detached task. Otherwise, if the
1043    /// caller dropped this future after the detached write produced a recoverable
1044    /// error but before the outer `.await` resumed, the recovery would be skipped: a
1045    /// `CorruptedChainState` error neither poisons nor evicts the worker, so the chain
1046    /// would be left at a partial tip serving stale reads with no `RevertConfirm`
1047    /// retransmission. Running recovery in the detached task makes it survive caller
1048    /// cancellation, just like the write itself.
1049    async fn chain_write<R, F, Fut>(&self, chain_id: ChainId, f: F) -> Result<R, WorkerError>
1050    where
1051        F: FnOnce(handle::RollbackGuard<StorageClient>) -> Fut
1052            + linera_base::task::MaybeSend
1053            + 'static,
1054        Fut: std::future::Future<Output = Result<R, WorkerError>> + linera_base::task::MaybeSend,
1055        R: linera_base::task::MaybeSend + 'static,
1056    {
1057        let state = self.get_or_create_chain_worker(chain_id).await?;
1058        let this = self.clone();
1059        Box::pin(wrap_future(linera_base::task::run_detached(async move {
1060            let result = async {
1061                let guard = handle::write_lock(&state).await?;
1062                f(guard).await
1063            }
1064            .await;
1065            if let Err(error) = &result {
1066                if error.must_reload_view() {
1067                    this.evict_poisoned_worker(chain_id, &state);
1068                } else if error.indicates_corrupted_chain_state() {
1069                    this.spawn_reset_corrupted_chain_state(chain_id, state);
1070                }
1071            }
1072            result
1073        })))
1074        .await
1075    }
1076
1077    /// Spawns a detached task that re-acquires the write lock and recovers the
1078    /// chain from a detected state corruption. Running the recovery in a separate
1079    /// task ensures it survives cancellation of the originating request: if the
1080    /// caller's future is dropped mid-way through re-execution, the chain would
1081    /// otherwise be left at a partial tip and our safety snapshot would be lost.
1082    /// Generated `RevertConfirm` requests are dispatched via the installed
1083    /// shard-routing sender when present (sharded validators), or locally
1084    /// through `handle_cross_chain_request` otherwise (client nodes and tests).
1085    /// Errors are logged; the caller already has the original error to
1086    /// propagate.
1087    fn spawn_reset_corrupted_chain_state(
1088        &self,
1089        chain_id: ChainId,
1090        state: ChainWorkerArc<StorageClient>,
1091    ) where
1092        StorageClient: Clone,
1093    {
1094        let this = self.clone();
1095        linera_base::Task::spawn(async move {
1096            let requests = {
1097                let mut guard = match handle::write_lock(&state).await {
1098                    Ok(guard) => guard,
1099                    Err(error) => {
1100                        tracing::error!(
1101                            %chain_id, %error,
1102                            "Failed to acquire write lock to reset corrupted chain state"
1103                        );
1104                        return;
1105                    }
1106                };
1107                match guard.maybe_reset_corrupted_chain_state().await {
1108                    Ok(Some(requests)) => requests,
1109                    Ok(None) => return,
1110                    Err(error) => {
1111                        tracing::error!(
1112                            %chain_id, %error, "Failed to reset corrupted chain state"
1113                        );
1114                        return;
1115                    }
1116                }
1117            };
1118            if let Some(sender) = &this.outbound_cross_chain_sender {
1119                // Sharded validator path: let the RPC layer route each request to
1120                // the shard that owns the target chain.
1121                for request in requests {
1122                    sender(request);
1123                }
1124            } else {
1125                // No routing dispatcher is installed (client node or test), so all
1126                // involved chains are co-located on this worker. Dispatch locally
1127                // in a loop, following any cascading cross-chain requests.
1128                let mut queue = VecDeque::from(requests);
1129                while let Some(request) = queue.pop_front() {
1130                    match this.handle_cross_chain_request(request).await {
1131                        Ok(actions) => queue.extend(actions.cross_chain_requests),
1132                        Err(error) => {
1133                            warn!(
1134                                %chain_id, %error,
1135                                "Failed to dispatch cross-chain request after \
1136                                resetting corrupted chain state"
1137                            );
1138                        }
1139                    }
1140                }
1141            }
1142        })
1143        .forget();
1144    }
1145
1146    /// Evicts a poisoned chain worker from the cache, but only if the entry still
1147    /// points to the same instance. This avoids removing a fresh replacement that
1148    /// another task may have already loaded.
1149    fn evict_poisoned_worker(&self, chain_id: ChainId, poisoned: &ChainWorkerArc<StorageClient>) {
1150        tracing::warn!(%chain_id, "Evicting poisoned chain worker from cache");
1151        let pin = self.chain_workers.pin();
1152        let weak_poisoned = Arc::downgrade(poisoned);
1153        let removed = pin.remove_if(&chain_id, |_key, future| {
1154            future
1155                .peek()
1156                .and_then(|r| r.clone().ok())
1157                .is_some_and(|weak| weak.ptr_eq(&weak_poisoned))
1158        });
1159        if removed.is_err() {
1160            tracing::trace!(%chain_id, "Poisoned worker entry already replaced; skipping eviction");
1161        }
1162    }
1163
1164    /// Returns or creates the per-chain batch state.
1165    async fn get_or_create_chain_batch(
1166        &self,
1167        chain_id: ChainId,
1168    ) -> Result<(mpsc::UnboundedSender<BatchRequest>, Shared<BatchFuture>), WorkerError> {
1169        // Fast path: reuse an existing live driver. The pin guard is !Send,
1170        // so it must be dropped before any .await point.
1171        if let Some(batch_processor) = self.chain_batches.pin().get(&chain_id) {
1172            if let Some(future) = batch_processor.future.upgrade() {
1173                return Ok((batch_processor.sender.clone(), future));
1174            }
1175        }
1176        let (new_request_processor, new_future) = ChainBatchRequestProcessor::create(
1177            self.clone(),
1178            chain_id,
1179            self.chain_worker_config.cross_chain_batch_size_limit,
1180        );
1181        match self
1182            .chain_batches
1183            .pin()
1184            .compute(chain_id, |existing| match existing {
1185                Some((_, batch_processor)) => {
1186                    if let Some(future) = batch_processor.future.upgrade() {
1187                        papaya::Operation::Abort((batch_processor.sender.clone(), future))
1188                    } else {
1189                        papaya::Operation::Insert(new_request_processor.clone())
1190                    }
1191                }
1192                None => papaya::Operation::Insert(new_request_processor.clone()),
1193            }) {
1194            papaya::Compute::Aborted((sender, future)) => Ok((sender, future)),
1195            papaya::Compute::Inserted(_, batch_processor)
1196            | papaya::Compute::Updated {
1197                new: (_, batch_processor),
1198                ..
1199            } => Ok((batch_processor.sender.clone(), new_future)),
1200            papaya::Compute::Removed { .. } => unreachable!(),
1201        }
1202    }
1203
1204    /// Gets or creates a chain worker for the given chain.
1205    ///
1206    /// The oneshot channel is created outside the `compute` closure to keep
1207    /// the closure pure (papaya may call it more than once on CAS retry and
1208    /// may memoize the output). If the fast path hits, the unused channel is
1209    /// dropped harmlessly.
1210    ///
1211    /// Returns a type-erased future to keep `!Sync` intermediate types (e.g.
1212    /// `std::sync::mpsc::Receiver` from `handle::ServiceRuntimeActor::spawn`) out of
1213    /// the caller's future type.
1214    fn get_or_create_chain_worker(
1215        &self,
1216        chain_id: ChainId,
1217    ) -> std::pin::Pin<
1218        Box<
1219            impl std::future::Future<Output = Result<ChainWorkerArc<StorageClient>, WorkerError>> + '_,
1220        >,
1221    > {
1222        Box::pin(wrap_future(async move {
1223            loop {
1224                // Create the channel outside the closure so that the
1225                // sender/receiver always match regardless of CAS retries.
1226                let (sender, receiver) = oneshot::channel();
1227                let shared_receiver = receiver.shared();
1228
1229                // The papaya guard is !Send, so it must be dropped before
1230                // any .await point.
1231                let wait_or_sender = {
1232                    let pin = self.chain_workers.pin();
1233                    match pin.compute(chain_id, |existing| match existing {
1234                        Some((_, entry)) => match entry.peek() {
1235                            Some(Ok(weak)) => match weak.upgrade() {
1236                                Some(arc) => papaya::Operation::Abort(Ok(arc)),
1237                                None => papaya::Operation::Insert(shared_receiver.clone()),
1238                            },
1239                            Some(Err(_)) => papaya::Operation::Insert(shared_receiver.clone()),
1240                            None => papaya::Operation::Abort(Err(entry.clone())),
1241                        },
1242                        None => papaya::Operation::Insert(shared_receiver.clone()),
1243                    }) {
1244                        papaya::Compute::Aborted(Ok(arc), ..) => return Ok(arc),
1245                        papaya::Compute::Aborted(Err(wait), ..) => Either::Left(wait),
1246                        papaya::Compute::Inserted { .. } | papaya::Compute::Updated { .. } => {
1247                            Either::Right(sender)
1248                        }
1249                        papaya::Compute::Removed { .. } => unreachable!(),
1250                    }
1251                };
1252
1253                match wait_or_sender {
1254                    Either::Left(wait) => {
1255                        // Another task is loading. Await the shared future.
1256                        if let Ok(weak) = wait.await {
1257                            if let Some(arc) = weak.upgrade() {
1258                                return Ok(arc);
1259                            }
1260                        }
1261                        // Loading failed or worker already dead; retry.
1262                    }
1263                    Either::Right(sender) => {
1264                        // We claimed the loading slot. Load from storage.
1265                        // On success, send the Weak through the channel.
1266                        // On error, dropping sender wakes waiters so they can retry.
1267                        let worker = self.load_chain_worker(chain_id).await?;
1268                        if sender.send(Arc::downgrade(&worker)).is_err() {
1269                            tracing::error!(%chain_id, "Receiver dropped while loading worker state.");
1270                            continue;
1271                        }
1272                        return Ok(worker);
1273                    }
1274                }
1275            }
1276        }))
1277    }
1278
1279    /// Loads a chain worker state from storage and wraps it in an Arc.
1280    async fn load_chain_worker(
1281        &self,
1282        chain_id: ChainId,
1283    ) -> Result<ChainWorkerArc<StorageClient>, WorkerError> {
1284        let delivery_notifier = self
1285            .delivery_notifiers
1286            .lock()
1287            .unwrap()
1288            .entry(chain_id)
1289            .or_default()
1290            .clone();
1291
1292        // `chain_modes=None` means "no tracked/sender distinction" (validators) — treat
1293        // every chain as tracked so it routes through `config.ttl`, not the unset
1294        // `config.sender_chain_ttl` which would skip `spawn_keep_alive` entirely.
1295        let is_tracked = self.chain_modes.as_ref().is_none_or(|chain_modes| {
1296            chain_modes
1297                .read()
1298                .unwrap()
1299                .get(&chain_id)
1300                .is_some_and(ListeningMode::is_full)
1301        });
1302
1303        let (service_runtime_endpoint, service_runtime_task) =
1304            if self.chain_worker_config.long_lived_services {
1305                let actor =
1306                    handle::ServiceRuntimeActor::spawn(chain_id, self.storage.thread_pool()).await;
1307                (Some(actor.endpoint), Some(actor.task))
1308            } else {
1309                (None, None)
1310            };
1311
1312        let state = crate::chain_worker::state::ChainWorkerState::load(
1313            self.chain_worker_config.clone(),
1314            self.storage.clone(),
1315            self.block_cache.clone(),
1316            self.execution_state_cache.clone(),
1317            self.chain_modes.clone(),
1318            delivery_notifier,
1319            chain_id,
1320            service_runtime_endpoint,
1321            service_runtime_task,
1322        )
1323        .await?;
1324
1325        Ok(handle::create_chain_worker(
1326            state,
1327            is_tracked,
1328            &self.chain_worker_config,
1329        ))
1330    }
1331
1332    /// Tries to execute a block proposal with a policy for handling bundle failures.
1333    ///
1334    /// Returns the modified block (bundles may be rejected/removed), the executed block,
1335    /// chain info response, and resource tracker.
1336    #[instrument(level = "trace", skip(self, block))]
1337    pub async fn stage_block_execution(
1338        &self,
1339        block: ProposedBlock,
1340        round: Option<u32>,
1341        published_blobs: Vec<Blob>,
1342        policy: BundleExecutionPolicy,
1343    ) -> Result<
1344        (
1345            ProposedBlock,
1346            Block,
1347            ChainInfoResponse,
1348            ResourceTracker,
1349            HashSet<ChainId>,
1350        ),
1351        WorkerError,
1352    > {
1353        let chain_id = block.chain_id;
1354        self.chain_write(chain_id, move |mut guard| async move {
1355            guard
1356                .stage_block_execution(block, round, &published_blobs, policy)
1357                .await
1358        })
1359        .await
1360    }
1361
1362    /// Executes a [`Query`] for an application's state on a specific chain.
1363    ///
1364    /// If `block_hash` is specified, system will query the application's state
1365    /// at that block. If it doesn't exist, it uses latest state.
1366    #[instrument(level = "trace", skip(self, chain_id, query))]
1367    pub async fn query_application(
1368        &self,
1369        chain_id: ChainId,
1370        query: Query,
1371        block_hash: Option<CryptoHash>,
1372    ) -> Result<(QueryOutcome, BlockHeight), WorkerError> {
1373        self.chain_write(chain_id, move |mut guard| async move {
1374            guard.query_application(query, block_hash).await
1375        })
1376        .await
1377    }
1378
1379    #[instrument(level = "trace", skip(self, chain_id, application_id), fields(
1380        nickname = %self.nickname(),
1381        chain_id = %chain_id,
1382        application_id = %application_id
1383    ))]
1384    /// Returns the description of the given application on the given chain.
1385    pub async fn describe_application(
1386        &self,
1387        chain_id: ChainId,
1388        application_id: ApplicationId,
1389    ) -> Result<ApplicationDescription, WorkerError> {
1390        let state = self.get_or_create_chain_worker(chain_id).await?;
1391        let guard = handle::read_lock_initialized(&state).await?;
1392        guard.describe_application_readonly(application_id).await
1393    }
1394
1395    /// Processes a confirmed block (aka a commit).
1396    #[instrument(
1397        level = "trace",
1398        skip(self, certificate, notify_when_messages_are_delivered),
1399        fields(
1400            nickname = %self.nickname(),
1401            chain_id = %certificate.block().header.chain_id,
1402            block_height = %certificate.block().header.height
1403        )
1404    )]
1405    async fn process_confirmed_block(
1406        &self,
1407        certificate: ConfirmedBlockCertificate,
1408        mode: ProcessConfirmedBlockMode,
1409        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
1410    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
1411        let chain_id = certificate.block().header.chain_id;
1412        self.chain_write(chain_id, move |mut guard| async move {
1413            guard
1414                .process_confirmed_block(certificate, mode, notify_when_messages_are_delivered)
1415                .await
1416        })
1417        .await
1418    }
1419
1420    /// Processes a validated block issued from a multi-owner chain.
1421    #[instrument(level = "trace", skip(self, certificate), fields(
1422        nickname = %self.nickname(),
1423        chain_id = %certificate.block().header.chain_id,
1424        block_height = %certificate.block().header.height
1425    ))]
1426    async fn process_validated_block(
1427        &self,
1428        certificate: ValidatedBlockCertificate,
1429    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
1430        let chain_id = certificate.block().header.chain_id;
1431        self.chain_write(chain_id, move |mut guard| async move {
1432            guard.process_validated_block(certificate).await
1433        })
1434        .await
1435    }
1436
1437    /// Processes a leader timeout issued from a multi-owner chain.
1438    #[instrument(level = "trace", skip(self, certificate), fields(
1439        nickname = %self.nickname(),
1440        chain_id = %certificate.value().chain_id(),
1441        height = %certificate.value().height()
1442    ))]
1443    async fn process_timeout(
1444        &self,
1445        certificate: TimeoutCertificate,
1446    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
1447        let chain_id = certificate.value().chain_id();
1448        self.chain_write(chain_id, move |mut guard| async move {
1449            guard.process_timeout(certificate).await
1450        })
1451        .await
1452    }
1453
1454    /// Enqueues a cross-chain update request and cooperatively drives the
1455    /// batch-processing future until the result is ready.
1456    #[instrument(level = "trace", skip(self, origin, recipient, bundles), fields(
1457        nickname = %self.nickname(),
1458        origin = %origin,
1459        recipient = %recipient,
1460        num_bundles = %bundles.len()
1461    ))]
1462    async fn process_cross_chain_update(
1463        &self,
1464        origin: ChainId,
1465        recipient: ChainId,
1466        bundles: Vec<(Epoch, MessageBundle)>,
1467        previous_height: Option<BlockHeight>,
1468    ) -> Result<CrossChainUpdateResult, WorkerError> {
1469        let (result_sender, receiver) = oneshot::channel();
1470        let request = BatchRequest::Update {
1471            origin,
1472            bundles,
1473            previous_height,
1474            result_sender,
1475        };
1476        self.enqueue_and_drive(recipient, request, receiver).await
1477    }
1478
1479    /// Enqueues a confirmation request and cooperatively drives the
1480    /// batch-processing future until the result is ready.
1481    async fn confirm_updated_recipient(
1482        &self,
1483        sender: ChainId,
1484        recipient: ChainId,
1485        latest_height: BlockHeight,
1486    ) -> Result<NetworkActions, WorkerError> {
1487        let (result_sender, receiver) = oneshot::channel();
1488        let request = BatchRequest::Confirm {
1489            recipient,
1490            latest_height,
1491            result_sender,
1492        };
1493        self.enqueue_and_drive(sender, request, receiver).await
1494    }
1495
1496    /// Sends a [`BatchRequest`] to the per-chain driver and cooperatively
1497    /// polls the shared processing future until our result arrives.
1498    async fn enqueue_and_drive<R>(
1499        &self,
1500        chain_id: ChainId,
1501        request: BatchRequest,
1502        mut receiver: oneshot::Receiver<Result<R, WorkerError>>,
1503    ) -> Result<R, WorkerError> {
1504        let mut pending = Some(request);
1505        loop {
1506            let (sender, future) = self.get_or_create_chain_batch(chain_id).await?;
1507            if let Some(request) = pending.take() {
1508                if let Err(mpsc::error::SendError(request)) = sender.send(request) {
1509                    pending = Some(request);
1510                    continue; // Driver died; retry will create a new one.
1511                }
1512            }
1513            // Poll the receiver first (biased): if the result is already available,
1514            // return it immediately without driving the batch future further.
1515            match future::select(pin::pin!(&mut receiver), future).await {
1516                Either::Left((result, _)) => {
1517                    // `Ok(inner)` is the normal reply. `Err(Canceled)` means the
1518                    // sender was dropped without one — `chain_write` failed before
1519                    // `process_batch` ran (e.g. an already-poisoned worker, which it
1520                    // has now evicted). Surface it as `PoisonedWorker`; the next
1521                    // attempt loads a fresh worker.
1522                    return result.unwrap_or(Err(WorkerError::PoisonedWorker));
1523                }
1524                Either::Right(((), _)) => match receiver.try_recv() {
1525                    Ok(result) => return result,
1526                    Err(oneshot::error::TryRecvError::Empty) => {}
1527                    Err(oneshot::error::TryRecvError::Closed) => {
1528                        return Err(WorkerError::ChainError(Box::new(
1529                            ChainError::InternalError("batch driver stopped".into()),
1530                        )));
1531                    }
1532                },
1533            }
1534        }
1535    }
1536
1537    /// Returns a stored [`ConfirmedBlockCertificate`] for a chain's block.
1538    #[instrument(level = "trace", skip(self, chain_id, height), fields(
1539        nickname = %self.nickname(),
1540        chain_id = %chain_id,
1541        height = %height
1542    ))]
1543    #[cfg(with_testing)]
1544    pub async fn read_certificate(
1545        &self,
1546        chain_id: ChainId,
1547        height: BlockHeight,
1548    ) -> Result<Option<CacheArc<ConfirmedBlockCertificate>>, WorkerError> {
1549        let state = self.get_or_create_chain_worker(chain_id).await?;
1550        let guard = handle::read_lock_initialized(&state).await?;
1551        guard.read_certificate(height).await
1552    }
1553
1554    /// Test helper that runs `ChainWorkerState::select_message_bundles` for the given
1555    /// recipient chain.
1556    #[cfg(with_testing)]
1557    pub async fn select_message_bundles(
1558        &self,
1559        recipient: ChainId,
1560        origin: &ChainId,
1561        next_height_to_receive: BlockHeight,
1562        last_anticipated_block_height: Option<BlockHeight>,
1563        bundles: Vec<(Epoch, MessageBundle)>,
1564    ) -> Result<Vec<MessageBundle>, WorkerError> {
1565        let state = self.get_or_create_chain_worker(recipient).await?;
1566        let guard = handle::read_lock(&state).await?;
1567        guard
1568            .select_message_bundles(
1569                origin,
1570                next_height_to_receive,
1571                last_anticipated_block_height,
1572                bundles,
1573            )
1574            .await
1575    }
1576
1577    /// Test helper that runs `ChainWorkerState::reset_and_reexecute_chain` for the given
1578    /// chain (the same routine the corruption-recovery path invokes).
1579    #[cfg(with_testing)]
1580    pub async fn reset_and_reexecute_chain(
1581        &self,
1582        chain_id: ChainId,
1583    ) -> Result<Vec<CrossChainRequest>, WorkerError> {
1584        let state = self.get_or_create_chain_worker(chain_id).await?;
1585        let mut guard = handle::write_lock(&state).await?;
1586        guard.reset_and_reexecute_chain().await
1587    }
1588
1589    /// Returns a read-only view of the [`ChainStateView`] of a chain referenced by its
1590    /// [`ChainId`].
1591    ///
1592    /// The returned guard holds a read lock on the chain state, preventing writes for
1593    /// its lifetime. Multiple concurrent readers are allowed.
1594    #[instrument(level = "trace", skip(self), fields(
1595        nickname = %self.nickname(),
1596        chain_id = %chain_id
1597    ))]
1598    pub async fn chain_state_view(
1599        &self,
1600        chain_id: ChainId,
1601    ) -> Result<ChainStateViewReadGuard<StorageClient>, WorkerError> {
1602        let state = self.get_or_create_chain_worker(chain_id).await?;
1603        let guard = handle::read_lock(&state).await?;
1604        Ok(ChainStateViewReadGuard(OwnedRwLockReadGuard::map(
1605            guard,
1606            |s| s.chain(),
1607        )))
1608    }
1609
1610    #[instrument(skip_all, fields(
1611        nick = self.nickname(),
1612        chain_id = format!("{:.8}", proposal.content.block.chain_id),
1613        height = %proposal.content.block.height,
1614    ))]
1615    /// Handles a block proposal, validating it and preparing the chain to vote on it.
1616    pub async fn handle_block_proposal(
1617        &self,
1618        proposal: BlockProposal,
1619    ) -> (Result<ChainInfoResponse, WorkerError>, NetworkActions) {
1620        trace!("{} <-- {:?}", self.nickname(), proposal);
1621        #[cfg(with_metrics)]
1622        let round = proposal.content.round;
1623
1624        let chain_id = proposal.content.block.chain_id;
1625        // Delay if block timestamp is in the future but within grace period.
1626        let now = self.storage.clock().current_time();
1627        let block_timestamp = proposal.content.block.timestamp;
1628        let delta = block_timestamp.delta_since(now);
1629        let grace_period = TimeDelta::from_micros(
1630            u64::try_from(self.chain_worker_config.block_time_grace_period.as_micros())
1631                .unwrap_or(u64::MAX),
1632        );
1633        if delta > TimeDelta::ZERO && delta <= grace_period {
1634            self.storage.clock().sleep_until(block_timestamp).await;
1635        }
1636
1637        let outcome = self
1638            .chain_write(chain_id, move |mut guard| async move {
1639                Ok::<_, WorkerError>(guard.handle_block_proposal(proposal).await)
1640            })
1641            .await;
1642        let (result, actions) = match outcome {
1643            Ok((result, actions)) => (result, actions),
1644            Err(err) => (Err(err), NetworkActions::default()),
1645        };
1646        #[cfg(with_metrics)]
1647        if result.is_ok() {
1648            metrics::NUM_ROUNDS_IN_BLOCK_PROPOSAL
1649                .with_label_values(&[round.type_name()])
1650                .observe(round.number() as f64);
1651        }
1652        (result, actions)
1653    }
1654
1655    /// Processes a certificate, e.g. to extend a chain with a confirmed block.
1656    // Other fields will be included in the caller's span.
1657    #[instrument(skip_all, fields(
1658        chain_id = %certificate.value.chain_id,
1659        hash = %certificate.value.value_hash,
1660    ))]
1661    pub async fn handle_lite_certificate(
1662        &self,
1663        certificate: LiteCertificate<'_>,
1664        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
1665    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
1666        match self.full_certificate(certificate).await? {
1667            Either::Left(confirmed) => {
1668                Box::pin(self.handle_confirmed_certificate(
1669                    confirmed,
1670                    ProcessConfirmedBlockMode::Auto,
1671                    notify_when_messages_are_delivered,
1672                ))
1673                .await
1674            }
1675            Either::Right(validated) => {
1676                if let Some(notifier) = notify_when_messages_are_delivered {
1677                    // Nothing to wait for.
1678                    if let Err(()) = notifier.send(()) {
1679                        debug!("Failed to notify message delivery to caller (validation cert)");
1680                    }
1681                }
1682                Box::pin(self.handle_validated_certificate(validated)).await
1683            }
1684        }
1685    }
1686
1687    /// Processes a confirmed block certificate.
1688    #[instrument(skip_all, fields(
1689        nick = self.nickname(),
1690        chain_id = format!("{:.8}", certificate.block().header.chain_id),
1691        height = %certificate.block().header.height,
1692    ))]
1693    pub async fn handle_confirmed_certificate(
1694        &self,
1695        certificate: ConfirmedBlockCertificate,
1696        mode: ProcessConfirmedBlockMode,
1697        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
1698    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
1699        trace!("{} <-- {:?}", self.nickname(), certificate);
1700        #[cfg(with_metrics)]
1701        let metrics_data = metrics::MetricsData::new(&certificate);
1702
1703        #[allow(unused_variables)]
1704        let (info, actions, outcome) = Box::pin(self.process_confirmed_block(
1705            certificate,
1706            mode,
1707            notify_when_messages_are_delivered,
1708        ))
1709        .await?;
1710
1711        #[cfg(with_metrics)]
1712        if matches!(outcome, BlockOutcome::Processed) {
1713            metrics_data.record();
1714        }
1715        Ok((info, actions))
1716    }
1717
1718    /// Processes a validated block certificate.
1719    #[instrument(skip_all, fields(
1720        nick = self.nickname(),
1721        chain_id = format!("{:.8}", certificate.block().header.chain_id),
1722        height = %certificate.block().header.height,
1723    ))]
1724    pub async fn handle_validated_certificate(
1725        &self,
1726        certificate: ValidatedBlockCertificate,
1727    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
1728        trace!("{} <-- {:?}", self.nickname(), certificate);
1729
1730        #[cfg(with_metrics)]
1731        let round = certificate.round;
1732        #[cfg(with_metrics)]
1733        let cert_str = certificate.inner().to_log_str();
1734
1735        #[allow(unused_variables)]
1736        let (info, actions, outcome) = Box::pin(self.process_validated_block(certificate)).await?;
1737        #[cfg(with_metrics)]
1738        {
1739            if matches!(outcome, BlockOutcome::Processed) {
1740                metrics::NUM_ROUNDS_IN_CERTIFICATE
1741                    .with_label_values(&[cert_str, round.type_name()])
1742                    .observe(round.number() as f64);
1743            }
1744        }
1745        Ok((info, actions))
1746    }
1747
1748    /// Processes a timeout certificate
1749    #[instrument(skip_all, fields(
1750        nick = self.nickname(),
1751        chain_id = format!("{:.8}", certificate.inner().chain_id()),
1752        height = %certificate.inner().height(),
1753    ))]
1754    pub async fn handle_timeout_certificate(
1755        &self,
1756        certificate: TimeoutCertificate,
1757    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
1758        trace!("{} <-- {:?}", self.nickname(), certificate);
1759        self.process_timeout(certificate).await
1760    }
1761
1762    #[instrument(skip_all, fields(
1763        nick = self.nickname(),
1764        chain_id = format!("{:.8}", query.chain_id)
1765    ))]
1766    /// Handles a query about a chain's state and returns the requested information.
1767    pub async fn handle_chain_info_query(
1768        &self,
1769        query: ChainInfoQuery,
1770    ) -> Result<ChainInfoResponse, WorkerError> {
1771        trace!("{} <-- {:?}", self.nickname(), query);
1772        #[cfg(with_metrics)]
1773        metrics::CHAIN_INFO_QUERIES.inc();
1774        let chain_id = query.chain_id;
1775        let result = self
1776            .chain_write(chain_id, move |mut guard| async move {
1777                guard.handle_chain_info_query(query).await
1778            })
1779            .await;
1780        trace!("{} --> {:?}", self.nickname(), result);
1781        result
1782    }
1783
1784    #[instrument(skip_all, fields(
1785        nick = self.nickname(),
1786        chain_id = format!("{:.8}", chain_id)
1787    ))]
1788    /// Downloads a blob that a pending block on the given chain depends on.
1789    pub async fn download_pending_blob(
1790        &self,
1791        chain_id: ChainId,
1792        blob_id: BlobId,
1793    ) -> Result<CacheArc<Blob>, WorkerError> {
1794        trace!("{} <-- download_pending_blob({blob_id:8})", self.nickname());
1795        let result = self
1796            .chain_read(chain_id, |guard| async move {
1797                guard.download_pending_blob(blob_id).await
1798            })
1799            .await;
1800        trace!(
1801            "{} --> {:?}",
1802            self.nickname(),
1803            result.as_ref().map(|_| blob_id)
1804        );
1805        result
1806    }
1807
1808    #[instrument(skip_all, fields(
1809        nick = self.nickname(),
1810        chain_id = format!("{:.8}", chain_id)
1811    ))]
1812    /// Handles a blob that a pending block depends on, adding it to the chain worker.
1813    pub async fn handle_pending_blob(
1814        &self,
1815        chain_id: ChainId,
1816        blob: Blob,
1817    ) -> Result<ChainInfoResponse, WorkerError> {
1818        let blob_id = blob.id();
1819        trace!("{} <-- handle_pending_blob({blob_id:8})", self.nickname());
1820        let result = self
1821            .chain_write(chain_id, move |mut guard| async move {
1822                guard.handle_pending_blob(blob).await
1823            })
1824            .await;
1825        trace!(
1826            "{} --> {:?}",
1827            self.nickname(),
1828            result.as_ref().map(|_| blob_id)
1829        );
1830        result
1831    }
1832
1833    #[instrument(skip_all, fields(
1834        nick = self.nickname(),
1835        chain_id = format!("{:.8}", request.target_chain_id())
1836    ))]
1837    /// Handles a cross-chain request received from another chain or shard.
1838    pub async fn handle_cross_chain_request(
1839        &self,
1840        request: CrossChainRequest,
1841    ) -> Result<NetworkActions, WorkerError> {
1842        trace!("{} <-- {:?}", self.nickname(), request);
1843        match request {
1844            CrossChainRequest::UpdateRecipient {
1845                sender,
1846                recipient,
1847                bundles,
1848                previous_height,
1849            } => {
1850                let mut actions = NetworkActions::default();
1851                let origin = sender;
1852                match self
1853                    .process_cross_chain_update(origin, recipient, bundles, previous_height)
1854                    .await?
1855                {
1856                    CrossChainUpdateResult::NothingToDo => {}
1857                    CrossChainUpdateResult::Updated(height) => {
1858                        actions.notifications.push(Notification {
1859                            chain_id: recipient,
1860                            reason: Reason::NewIncomingBundle { origin, height },
1861                        });
1862                        actions.cross_chain_requests.push(
1863                            CrossChainRequest::ConfirmUpdatedRecipient {
1864                                sender,
1865                                recipient,
1866                                latest_height: height,
1867                            },
1868                        );
1869                    }
1870                    CrossChainUpdateResult::GapDetected {
1871                        origin,
1872                        retransmit_from,
1873                    } => {
1874                        actions
1875                            .cross_chain_requests
1876                            .push(CrossChainRequest::RevertConfirm {
1877                                sender: origin,
1878                                recipient,
1879                                retransmit_from,
1880                            });
1881                    }
1882                }
1883                Ok(actions)
1884            }
1885            CrossChainRequest::ConfirmUpdatedRecipient {
1886                sender,
1887                recipient,
1888                latest_height,
1889            } => {
1890                let actions = self
1891                    .confirm_updated_recipient(sender, recipient, latest_height)
1892                    .await?;
1893                Ok(actions)
1894            }
1895            CrossChainRequest::RevertConfirm {
1896                sender,
1897                recipient,
1898                retransmit_from,
1899            } => {
1900                self.chain_write(sender, move |mut guard| async move {
1901                    guard
1902                        .handle_revert_confirm(recipient, retransmit_from)
1903                        .await
1904                })
1905                .await
1906            }
1907        }
1908    }
1909
1910    /// Updates the received certificate trackers to at least the given values.
1911    #[instrument(skip_all, fields(
1912        nickname = %self.nickname(),
1913        chain_id = %chain_id,
1914        num_trackers = %new_trackers.len()
1915    ))]
1916    pub async fn update_received_certificate_trackers(
1917        &self,
1918        chain_id: ChainId,
1919        new_trackers: BTreeMap<ValidatorPublicKey, u64>,
1920    ) -> Result<(), WorkerError> {
1921        self.chain_write(chain_id, move |mut guard| async move {
1922            guard
1923                .update_received_certificate_trackers(new_trackers)
1924                .await
1925        })
1926        .await
1927    }
1928
1929    /// Gets preprocessed block hashes in a given height range.
1930    #[instrument(skip_all, fields(
1931        nickname = %self.nickname(),
1932        chain_id = %chain_id,
1933        start = %start,
1934        end = %end
1935    ))]
1936    pub async fn get_preprocessed_block_hashes(
1937        &self,
1938        chain_id: ChainId,
1939        start: BlockHeight,
1940        end: BlockHeight,
1941    ) -> Result<Vec<CryptoHash>, WorkerError> {
1942        self.chain_read(chain_id, |guard| async move {
1943            guard.get_preprocessed_block_hashes(start, end).await
1944        })
1945        .await
1946    }
1947
1948    /// Gets the next block height to receive from an inbox.
1949    #[instrument(skip_all, fields(
1950        nickname = %self.nickname(),
1951        chain_id = %chain_id,
1952        origin = %origin
1953    ))]
1954    pub async fn get_inbox_next_height(
1955        &self,
1956        chain_id: ChainId,
1957        origin: ChainId,
1958    ) -> Result<BlockHeight, WorkerError> {
1959        self.chain_read(chain_id, |guard| async move {
1960            guard.get_inbox_next_height(origin).await
1961        })
1962        .await
1963    }
1964
1965    /// Gets locking blobs for specific blob IDs.
1966    /// Returns `Ok(None)` if any of the blobs is not found.
1967    #[instrument(skip_all, fields(
1968        nickname = %self.nickname(),
1969        chain_id = %chain_id,
1970        num_blob_ids = %blob_ids.len()
1971    ))]
1972    pub async fn get_locking_blobs(
1973        &self,
1974        chain_id: ChainId,
1975        blob_ids: Vec<BlobId>,
1976    ) -> Result<Option<Vec<Blob>>, WorkerError> {
1977        self.chain_read(chain_id, |guard| async move {
1978            guard.get_locking_blobs(blob_ids).await
1979        })
1980        .await
1981    }
1982
1983    /// Gets block hashes for the given heights.
1984    pub async fn get_block_hashes(
1985        &self,
1986        chain_id: ChainId,
1987        heights: Vec<BlockHeight>,
1988    ) -> Result<Vec<CryptoHash>, WorkerError> {
1989        self.chain_read(chain_id, |guard| async move {
1990            guard.get_block_hashes(heights).await
1991        })
1992        .await
1993    }
1994
1995    /// Gets proposed blobs from the manager for specified blob IDs.
1996    pub async fn get_proposed_blobs(
1997        &self,
1998        chain_id: ChainId,
1999        blob_ids: Vec<BlobId>,
2000    ) -> Result<Vec<Blob>, WorkerError> {
2001        self.chain_read(chain_id, |guard| async move {
2002            guard.get_proposed_blobs(blob_ids).await
2003        })
2004        .await
2005    }
2006
2007    /// Gets event subscriptions from the chain.
2008    pub async fn get_event_subscriptions(
2009        &self,
2010        chain_id: ChainId,
2011    ) -> Result<EventSubscriptionsResult, WorkerError> {
2012        self.chain_read(chain_id, |guard| async move {
2013            guard.get_event_subscriptions().await
2014        })
2015        .await
2016    }
2017
2018    /// Gets a stream's [`StreamCounts`]: its next expected event index and its lowest readable
2019    /// index, read from a single chain state view so they are mutually consistent.
2020    pub async fn get_stream_indices(
2021        &self,
2022        chain_id: ChainId,
2023        stream_id: StreamId,
2024    ) -> Result<StreamCounts, WorkerError> {
2025        self.chain_read(chain_id, |guard| async move {
2026            guard.get_stream_indices(stream_id).await
2027        })
2028        .await
2029    }
2030
2031    /// Gets the `next_expected_events` indices for the given streams.
2032    pub async fn next_expected_events(
2033        &self,
2034        chain_id: ChainId,
2035        stream_ids: Vec<StreamId>,
2036    ) -> Result<BTreeMap<StreamId, u32>, WorkerError> {
2037        self.chain_read(chain_id, |guard| async move {
2038            guard.get_next_expected_events(stream_ids).await
2039        })
2040        .await
2041    }
2042
2043    /// Gets received certificate trackers.
2044    pub async fn get_received_certificate_trackers(
2045        &self,
2046        chain_id: ChainId,
2047    ) -> Result<HashMap<ValidatorPublicKey, u64>, WorkerError> {
2048        self.chain_read(chain_id, |guard| async move {
2049            guard.get_received_certificate_trackers().await
2050        })
2051        .await
2052    }
2053
2054    /// Returns the pending cross-chain network actions for this chain without
2055    /// initializing its execution state. Safe to call on chains whose
2056    /// `ChainDescription` blob is not available locally.
2057    pub async fn cross_chain_network_actions(
2058        &self,
2059        chain_id: ChainId,
2060    ) -> Result<NetworkActions, WorkerError> {
2061        // Fast path: when the outbox index is already reconciled to the current tracked set,
2062        // the network actions are built from a read-only view, so a shared lock with no save
2063        // suffices — avoiding the write lock, task spawn and `save()` of the slow path.
2064        if let Some(actions) = self
2065            .chain_read(chain_id, |guard| async move {
2066                guard.cross_chain_network_actions_if_reconciled().await
2067            })
2068            .await?
2069        {
2070            return Ok(actions);
2071        }
2072        // Slow path (first load after migration, or the tracked set changed): reconcile and
2073        // persist the index under an exclusive lock before building.
2074        self.chain_write(chain_id, |mut guard| async move {
2075            guard.reconcile_and_cross_chain_network_actions().await
2076        })
2077        .await
2078    }
2079
2080    /// Gets tip state and outbox info for next_outbox_heights calculation.
2081    pub async fn get_tip_state_and_outbox_info(
2082        &self,
2083        chain_id: ChainId,
2084        receiver_id: ChainId,
2085    ) -> Result<(BlockHeight, Option<BlockHeight>), WorkerError> {
2086        self.chain_read(chain_id, |guard| async move {
2087            guard.get_tip_state_and_outbox_info(receiver_id).await
2088        })
2089        .await
2090    }
2091
2092    /// Gets the next height to preprocess.
2093    pub async fn get_next_height_to_preprocess(
2094        &self,
2095        chain_id: ChainId,
2096    ) -> Result<BlockHeight, WorkerError> {
2097        self.chain_read(chain_id, |guard| async move {
2098            Ok(guard.get_next_height_to_preprocess())
2099        })
2100        .await
2101    }
2102}
2103
2104#[cfg(with_testing)]
2105impl<StorageClient> WorkerState<StorageClient>
2106where
2107    StorageClient: Storage + Clone + 'static,
2108{
2109    /// Gets a reference to the validator's [`ValidatorPublicKey`].
2110    ///
2111    /// # Panics
2112    ///
2113    /// If the validator doesn't have a key pair assigned to it.
2114    #[instrument(level = "trace", skip(self))]
2115    pub fn public_key(&self) -> ValidatorPublicKey {
2116        self.chain_worker_config
2117            .key_pair()
2118            .expect(
2119                "Test validator should have a key pair assigned to it \
2120                in order to obtain its public key",
2121            )
2122            .public()
2123    }
2124}