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