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