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