Skip to main content

linera_core/chain_worker/
state.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The state and functionality of a chain worker.
5
6use std::{
7    borrow::Cow,
8    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
9    sync::{self, Arc},
10};
11
12use futures::future::Either;
13#[cfg(with_metrics)]
14use linera_base::prometheus_util::MeasureLatency as _;
15use linera_base::{
16    crypto::{CryptoHash, ValidatorPublicKey},
17    data_types::{
18        ApplicationDescription, ArithmeticError, Blob, BlockHeight, Epoch, OracleResponse, Round,
19        Timestamp,
20    },
21    ensure,
22    hashed::Hashed,
23    identifiers::{AccountOwner, ApplicationId, BlobId, BlobType, ChainId, EventId, StreamId},
24};
25use linera_cache::{Arc as CacheArc, UniqueValueCache, ValueCache};
26use linera_chain::{
27    data_types::{
28        BlockProposal, BundleExecutionPolicy, IncomingBundle, MessageAction, MessageBundle,
29        OriginalProposal, ProposalContent, ProposedBlock,
30    },
31    manager::{self, ManagerSafetySnapshot},
32    types::{
33        Block, ConfirmedBlock, ConfirmedBlockCertificate, TimeoutCertificate,
34        ValidatedBlockCertificate,
35    },
36    BlockExecution, ChainError, ChainExecutionContext, ChainIdSet, ChainStateView, ChainTipState,
37    ExecutionResultExt as _, StreamCounts,
38};
39use linera_execution::{
40    system::{EpochEventData, EventSubscriptions, EPOCH_STREAM_NAME},
41    ExecutionRuntimeContext as _, ExecutionStateView, Query, QueryContext, QueryOutcome,
42    ResourceTracker, ServiceRuntimeEndpoint,
43};
44use linera_storage::{Clock as _, Storage};
45use linera_views::{
46    batch::Batch,
47    context::{Context, InactiveContext},
48    store::WritableKeyValueStore as _,
49    views::{ReplaceContext as _, RootView as _, View as _},
50};
51use tokio::sync::oneshot;
52use tracing::{debug, info, instrument, trace, warn};
53
54use crate::{
55    chain_worker::{
56        export::BlockExportHandle, handle::AtomicTimestamp, ChainWorkerConfig, DeliveryNotifier,
57    },
58    client::{ChainModes, ListeningMode},
59    data_types::{ChainInfo, ChainInfoQuery, ChainInfoResponse, CrossChainRequest},
60    worker::{BatchRequest, NetworkActions, Notification, Reason, WorkerError},
61};
62
63/// Type alias for event subscriptions result.
64pub(crate) type EventSubscriptionsResult = Vec<((ChainId, StreamId), EventSubscriptions)>;
65
66#[cfg(with_metrics)]
67pub(crate) mod metrics {
68    use linera_base::prometheus_util::{
69        exponential_bucket_interval, exponential_bucket_latencies, register_histogram,
70        register_histogram_vec, register_int_counter, register_int_counter_vec,
71    };
72    use prometheus::{Histogram, HistogramVec, IntCounter, IntCounterVec};
73
74    linera_base::declare_metrics! {
75        pub static CREATE_NETWORK_ACTIONS_LATENCY: Histogram =
76            register_histogram(
77                "create_network_actions_latency",
78                "Time (ms) to create network actions",
79                exponential_bucket_latencies(10_000.0),
80            );
81
82        pub static NUM_INBOXES: HistogramVec =
83            register_histogram_vec(
84                "num_inboxes",
85                "Number of inboxes",
86                &[],
87                exponential_bucket_interval(1.0, 10_000.0),
88            );
89
90        pub static RECEIVED_LOG_QUERY_ENTRIES: Histogram =
91            register_histogram(
92                "received_log_query_entries",
93                "Number of received-log entries returned per chain info query that asks for them",
94                exponential_bucket_interval(1.0, 100_000.0),
95            );
96
97        pub static BLOCK_PROPOSALS_RECEIVED_TOTAL: IntCounter =
98            register_int_counter(
99                "block_proposals_received_total",
100                "Total number of block proposals received by the worker",
101            );
102
103        pub static BLOCK_PROPOSALS_REJECTED_TOTAL: IntCounterVec =
104            register_int_counter_vec(
105                "block_proposals_rejected_total",
106                "Total number of block proposals rejected by the worker, labelled by error type",
107                &["error_type"],
108            );
109    }
110}
111
112/// The state of the chain worker.
113pub(crate) struct ChainWorkerState<StorageClient>
114where
115    StorageClient: Storage,
116{
117    config: ChainWorkerConfig,
118    storage: StorageClient,
119    chain: ChainStateView<StorageClient::Context>,
120    service_runtime_endpoint: Option<ServiceRuntimeEndpoint>,
121    /// The background task running the service runtime. Must be kept alive for the
122    /// lifetime of the worker: the pool `Guard` wrapper returns the thread-pool slot
123    /// when dropped, so dropping this early lets the pool schedule unrelated work on a
124    /// thread that is still running the service runtime.
125    service_runtime_task: Option<web_thread_pool::Task<()>>,
126    /// Timestamp of the last access.
127    /// Used by the keep-alive task to determine when the worker has been idle.
128    /// Wrapped in `Arc` so the keep-alive task can read it without acquiring
129    /// the `RwLock`.
130    last_access: Arc<AtomicTimestamp>,
131    block_values: Arc<ValueCache<CryptoHash, ConfirmedBlock>>,
132    execution_state_cache:
133        Option<Arc<UniqueValueCache<CryptoHash, ExecutionStateView<InactiveContext>>>>,
134    chain_modes: Option<Arc<sync::RwLock<ChainModes>>>,
135    delivery_notifier: DeliveryNotifier,
136    knows_chain_is_active: bool,
137    /// Set to `true` if a database `save` failure has left storage potentially
138    /// inconsistent.
139    poisoned: bool,
140    /// The process-wide export queue, if the server enabled block export.
141    block_export: Option<BlockExportHandle>,
142    /// When export progress was last folded into `exported_heights`, to bound the register
143    /// rewrites on an active chain.
144    last_exported_heights_fold: Option<linera_base::time::Instant>,
145}
146
147/// The result of processing a cross-chain update.
148pub(crate) enum CrossChainUpdateResult {
149    /// The update was applied up to the given height. The caller must save.
150    Updated(BlockHeight),
151    /// All bundles were already received; nothing to do.
152    NothingToDo,
153    /// A gap was detected in the inbox for messages from `origin`. If
154    /// `allow_revert_confirm` is enabled, a `RevertConfirm` request should be sent
155    /// to retransmit bundles starting from `retransmit_from`.
156    GapDetected {
157        origin: ChainId,
158        retransmit_from: BlockHeight,
159    },
160}
161
162/// Whether the block was processed or skipped. Used for metrics.
163pub enum BlockOutcome {
164    Processed,
165    Preprocessed,
166    Skipped,
167}
168
169/// How to handle a confirmed block.
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum ProcessConfirmedBlockMode {
172    /// Execute the block if it is contiguous (or bridgeable via a checkpoint);
173    /// otherwise preprocess it. Used by validators and by any caller that
174    /// wants graceful fallback when there's a gap.
175    Auto,
176    /// Execute the block. Fail with [`WorkerError::InvalidBlockChaining`] if
177    /// there's a gap that can't be bridged. Use when the caller knows the
178    /// block must be contiguous and wants a hard error otherwise.
179    Execute,
180    /// Only preprocess the block, never execute it — even if it would be
181    /// contiguous. Used by the client for sender-chain blocks it is not
182    /// otherwise tracking, since their execution state is irrelevant.
183    Preprocess,
184}
185
186impl<StorageClient> ChainWorkerState<StorageClient>
187where
188    StorageClient: Storage + Clone + 'static,
189{
190    /// Creates a new [`ChainWorkerState`] using the provided `storage` client.
191    #[instrument(skip_all, fields(
192        chain_id = %chain_id
193    ))]
194    #[expect(clippy::too_many_arguments)]
195    pub(crate) async fn load(
196        config: ChainWorkerConfig,
197        storage: StorageClient,
198        block_values: Arc<ValueCache<CryptoHash, ConfirmedBlock>>,
199        execution_state_cache: Option<
200            Arc<UniqueValueCache<CryptoHash, ExecutionStateView<InactiveContext>>>,
201        >,
202        chain_modes: Option<Arc<sync::RwLock<ChainModes>>>,
203        delivery_notifier: DeliveryNotifier,
204        chain_id: ChainId,
205        service_runtime_endpoint: Option<ServiceRuntimeEndpoint>,
206        service_runtime_task: Option<web_thread_pool::Task<()>>,
207        block_export: Option<BlockExportHandle>,
208    ) -> Result<Self, WorkerError> {
209        let chain = storage.load_chain(chain_id).await?;
210
211        Ok(ChainWorkerState {
212            config,
213            storage,
214            chain,
215            service_runtime_endpoint,
216            service_runtime_task,
217            last_access: Arc::new(AtomicTimestamp::now()),
218            block_values,
219            execution_state_cache,
220            chain_modes,
221            delivery_notifier,
222            knows_chain_is_active: false,
223            poisoned: false,
224            block_export,
225            last_exported_heights_fold: None,
226        })
227    }
228
229    /// Returns the [`ChainId`] of the chain handled by this worker.
230    fn chain_id(&self) -> ChainId {
231        self.chain.chain_id()
232    }
233
234    /// Returns a reference to the chain state view.
235    pub(crate) fn chain(&self) -> &ChainStateView<StorageClient::Context> {
236        &self.chain
237    }
238
239    /// Resolves the committee that signed certificates at the given epoch by
240    /// walking the admin chain's epoch event stream — works even after the
241    /// epoch has been revoked, so long as the admin chain still has the event.
242    /// Surfaces `EventsNotFound` (as a chained `ChainError::ExecutionError`)
243    /// when the admin event isn't local, so the caller's retry/sync paths can
244    /// fetch it before re-asking.
245    async fn committee_for_epoch(
246        &self,
247        epoch: Epoch,
248    ) -> Result<linera_execution::committee::Committee, WorkerError> {
249        let hash = self
250            .chain
251            .execution_state
252            .context()
253            .extra()
254            .get_committee_hashes(epoch..=epoch)
255            .await
256            .map_err(|error| {
257                ChainError::ExecutionError(Box::new(error), ChainExecutionContext::Block)
258            })?
259            .remove(&epoch)
260            .ok_or_else(|| {
261                ChainError::InternalError(format!(
262                    "missing committee for epoch {epoch}; this is a bug"
263                ))
264            })?;
265        let committee = self
266            .chain
267            .execution_state
268            .context()
269            .extra()
270            .get_or_load_committee_by_hash(hash)
271            .await
272            .map_err(|error| {
273                ChainError::ExecutionError(Box::new(error), ChainExecutionContext::Block)
274            })?;
275        Ok((*committee).clone())
276    }
277
278    /// Filters bundles destined for this chain to drop ones already received and to refuse
279    /// ones whose epoch has been revoked on the admin chain.
280    ///
281    /// A revoked-epoch bundle is still accepted if (a) it has already been executed by
282    /// anticipation (`bundle.height <= last_anticipated_block_height`), or (b) a later
283    /// bundle in the same batch is in a still-trusted epoch — that bundle's certificate
284    /// transitively re-certifies all preceding ones via prev-hash chaining.
285    pub(crate) async fn select_message_bundles(
286        &self,
287        origin: &ChainId,
288        next_height_to_receive: BlockHeight,
289        last_anticipated_block_height: Option<BlockHeight>,
290        mut bundles: Vec<(Epoch, MessageBundle)>,
291    ) -> Result<Vec<MessageBundle>, WorkerError> {
292        let recipient = self.chain_id();
293        let mut latest_height = None;
294        let mut skipped_len = 0;
295        let mut trusted_len = 0;
296        for (i, (epoch, bundle)) in bundles.iter().enumerate() {
297            ensure!(
298                latest_height <= Some(bundle.height),
299                WorkerError::InvalidCrossChainRequest
300            );
301            latest_height = Some(bundle.height);
302            if bundle.height < next_height_to_receive {
303                skipped_len = i + 1;
304            }
305            let is_revoked = self
306                .storage
307                .is_epoch_revoked(*epoch)
308                .await
309                .map_err(|error| {
310                    WorkerError::ChainError(Box::new(ChainError::ExecutionError(
311                        Box::new(error),
312                        ChainExecutionContext::Block,
313                    )))
314                })?;
315            if !is_revoked || Some(bundle.height) <= last_anticipated_block_height {
316                trusted_len = i + 1;
317            }
318        }
319        if skipped_len > 0 {
320            let (_, sample_bundle) = &bundles[skipped_len - 1];
321            debug!(
322                "Ignoring repeated messages to {recipient:.8} from {origin:} at height {}",
323                sample_bundle.height,
324            );
325        }
326        if skipped_len < bundles.len() && trusted_len < bundles.len() {
327            let (sample_epoch, sample_bundle) = &bundles[trusted_len];
328            warn!(
329                "Refusing messages to {recipient:.8} from {origin:} at height {} \
330                 because the epoch {} is not trusted any more",
331                sample_bundle.height, sample_epoch,
332            );
333        }
334        Ok(if skipped_len < trusted_len {
335            bundles
336                .drain(skipped_len..trusted_len)
337                .map(|(_, bundle)| bundle)
338                .collect()
339        } else {
340            vec![]
341        })
342    }
343
344    /// Returns whether this chain is known to be active (initialized).
345    pub(crate) fn knows_chain_is_active(&self) -> bool {
346        self.knows_chain_is_active
347    }
348
349    /// Rolls back any uncommitted changes to the chain state.
350    pub(crate) fn rollback(&mut self) {
351        self.chain.rollback();
352    }
353
354    /// Returns `WorkerError::PoisonedWorker` if the worker is poisoned due to a database
355    /// `save` failure.
356    pub(crate) fn check_not_poisoned(&self) -> Result<(), WorkerError> {
357        ensure!(!self.poisoned, WorkerError::PoisonedWorker);
358        Ok(())
359    }
360
361    /// Updates the last-access timestamp to the current time.
362    pub(crate) fn touch(&self) {
363        self.last_access.store_now();
364    }
365
366    /// Returns a clone of the last-access `Arc`, for use by the keep-alive task.
367    pub(crate) fn last_access_arc(&self) -> Arc<AtomicTimestamp> {
368        Arc::clone(&self.last_access)
369    }
370
371    /// Drops the service runtime endpoint, signaling the runtime task to stop.
372    /// Returns the runtime task so the caller can await it outside the lock.
373    pub(crate) fn clear_service_runtime(&mut self) -> Option<web_thread_pool::Task<()>> {
374        self.service_runtime_endpoint.take();
375        self.service_runtime_task.take()
376    }
377
378    /// Returns the pending cross-chain network actions if the outbox index is already
379    /// reconciled to the current tracked set, or `None` if it must first be reconciled (which
380    /// needs a write lock).
381    pub(crate) async fn cross_chain_network_actions_if_reconciled(
382        &self,
383    ) -> Result<Option<NetworkActions>, WorkerError> {
384        let tracked = self.tracked_full_chains();
385        if !self.chain.outbox_index_is_reconciled(tracked.as_deref()) {
386            return Ok(None);
387        }
388        Ok(Some(
389            self.build_network_actions(None, tracked.as_deref().map(|h| h.inner()))
390                .await?,
391        ))
392    }
393
394    /// Reconciles the outbox index with the current tracked set, then returns the pending
395    /// cross-chain network actions for this chain, without initializing the chain's execution
396    /// state. Intended for callers that only need to re-emit cross-chain requests from the
397    /// outbox of a sender chain whose `ChainDescription` we may never have needed.
398    ///
399    /// This is the slow path of [`Self::cross_chain_network_actions_if_reconciled`].
400    #[instrument(skip_all, fields(chain_id = %self.chain_id()))]
401    pub(crate) async fn reconcile_and_cross_chain_network_actions(
402        &mut self,
403    ) -> Result<NetworkActions, WorkerError> {
404        let tracked = self.tracked_full_chains();
405        self.chain
406            .reconcile_outbox_index(tracked.as_deref())
407            .await?;
408        let actions = self
409            .build_network_actions(None, tracked.as_deref().map(|h| h.inner()))
410            .await?;
411        self.save().await?;
412        Ok(actions)
413    }
414
415    /// Handles a [`ChainInfoQuery`], potentially voting on the next block.
416    #[tracing::instrument(level = "debug", skip(self))]
417    pub(crate) async fn handle_chain_info_query(
418        &mut self,
419        query: ChainInfoQuery,
420    ) -> Result<ChainInfoResponse, WorkerError> {
421        if let Some((height, round)) = query.request_leader_timeout {
422            self.vote_for_leader_timeout(height, round).await?;
423        }
424        if query.request_fallback {
425            self.vote_for_fallback().await?;
426        }
427        self.prepare_chain_info_response(query).await
428    }
429
430    /// Returns the requested blob, if it belongs to the current locking block or pending proposal.
431    #[instrument(skip_all, fields(
432        chain_id = %self.chain_id(),
433        blob_id = %blob_id
434    ))]
435    pub(crate) async fn download_pending_blob(
436        &self,
437        blob_id: BlobId,
438    ) -> Result<CacheArc<Blob>, WorkerError> {
439        if let Some(blob) = self.chain.manager.pending_blob(&blob_id).await? {
440            return Ok(self.storage.cache_blob(blob));
441        }
442        self.storage
443            .read_blob(blob_id)
444            .await?
445            .ok_or(WorkerError::BlobsNotFound(vec![blob_id]))
446    }
447
448    /// Reads the blobs from the chain manager or from storage. Returns an error if any are
449    /// missing.
450    #[instrument(skip_all, fields(
451        chain_id = %self.chain_id()
452    ))]
453    async fn get_required_blobs(
454        &self,
455        required_blob_ids: impl IntoIterator<Item = BlobId>,
456        created_blobs: BTreeMap<BlobId, Blob>,
457    ) -> Result<BTreeMap<BlobId, Blob>, WorkerError> {
458        let maybe_blobs = self
459            .maybe_get_required_blobs(required_blob_ids, Some(created_blobs))
460            .await?;
461        let not_found_blob_ids = missing_blob_ids(&maybe_blobs);
462        ensure!(
463            not_found_blob_ids.is_empty(),
464            WorkerError::BlobsNotFound(not_found_blob_ids)
465        );
466        Ok(maybe_blobs
467            .into_iter()
468            .filter_map(|(blob_id, maybe_blob)| Some((blob_id, maybe_blob?)))
469            .collect())
470    }
471
472    /// Tries to read the blobs from the chain manager or storage. Returns `None` if not found.
473    #[instrument(skip_all, fields(
474        chain_id = %self.chain_id()
475    ))]
476    async fn maybe_get_required_blobs(
477        &self,
478        blob_ids: impl IntoIterator<Item = BlobId>,
479        mut created_blobs: Option<BTreeMap<BlobId, Blob>>,
480    ) -> Result<BTreeMap<BlobId, Option<Blob>>, WorkerError> {
481        let maybe_blobs = blob_ids.into_iter().collect::<BTreeSet<_>>();
482        let mut maybe_blobs = maybe_blobs
483            .into_iter()
484            .map(|x| (x, None))
485            .collect::<Vec<(BlobId, Option<Blob>)>>();
486
487        if let Some(blob_map) = &mut created_blobs {
488            for (blob_id, value) in &mut maybe_blobs {
489                if let Some(blob) = blob_map.remove(blob_id) {
490                    *value = Some(blob);
491                }
492            }
493        }
494
495        let (missing_indices, missing_blob_ids) = missing_indices_blob_ids(&maybe_blobs);
496        let second_block_blobs = self.chain.manager.pending_blobs(&missing_blob_ids).await?;
497        for (index, blob) in missing_indices.into_iter().zip(second_block_blobs) {
498            maybe_blobs[index].1 = blob;
499        }
500
501        let (missing_indices, missing_blob_ids) = missing_indices_blob_ids(&maybe_blobs);
502        let third_block_blobs = self
503            .chain
504            .pending_validated_blobs
505            .multi_get(&missing_blob_ids)
506            .await?;
507        for (index, blob) in missing_indices.into_iter().zip(third_block_blobs) {
508            maybe_blobs[index].1 = blob;
509        }
510
511        let (missing_indices, missing_blob_ids) = missing_indices_blob_ids(&maybe_blobs);
512        if !missing_indices.is_empty() {
513            let all_entries_pending_blobs = self
514                .chain
515                .pending_proposed_blobs
516                .try_load_all_entries()
517                .await?;
518            for (index, blob_id) in missing_indices.into_iter().zip(missing_blob_ids) {
519                for (_, pending_blobs) in &all_entries_pending_blobs {
520                    if let Some(blob) = pending_blobs.get(&blob_id).await? {
521                        maybe_blobs[index].1 = Some(blob);
522                        break;
523                    }
524                }
525            }
526        }
527
528        let (missing_indices, missing_blob_ids) = missing_indices_blob_ids(&maybe_blobs);
529        let fourth_block_blobs = self.storage.read_blobs(&missing_blob_ids).await?;
530        for (index, blob) in missing_indices.into_iter().zip(fourth_block_blobs) {
531            maybe_blobs[index].1 = blob.map(CacheArc::unwrap_or_clone);
532        }
533        Ok(maybe_blobs.into_iter().collect())
534    }
535
536    /// Creates cross-chain requests for a single recipient from its outbox.
537    #[instrument(skip_all, fields(
538        chain_id = %self.chain_id()
539    ))]
540    async fn create_cross_chain_actions_for_recipient(
541        &self,
542        recipient: ChainId,
543    ) -> Result<NetworkActions, WorkerError> {
544        let outbox = self.chain.outboxes.try_load_entry(&recipient).await?;
545        let Some(outbox) = outbox else {
546            return Ok(NetworkActions::default());
547        };
548        let heights = outbox.queue.elements().await?;
549        if heights.is_empty() {
550            return Ok(NetworkActions::default());
551        }
552        let heights_by_recipient = BTreeMap::from([(recipient, heights)]);
553        let cross_chain_requests = self
554            .create_cross_chain_requests(heights_by_recipient)
555            .await?;
556        Ok(NetworkActions {
557            cross_chain_requests,
558            notifications: Vec::new(),
559        })
560    }
561
562    /// Returns the set of chains tracked in full mode together with its memoized hash, or `None` if
563    /// every chain is implicitely in full-mode (e.g. on validator).
564    fn tracked_full_chains(&self) -> Option<Arc<Hashed<ChainIdSet>>> {
565        let chain_modes = self.chain_modes.as_ref()?;
566        let full = chain_modes
567            .read()
568            .expect("Panics should not happen while holding a lock to `chain_modes`")
569            .full();
570        Some(full)
571    }
572
573    /// Returns whether the given chain is tracked in full mode (always `true` on a validator),
574    /// i.e. whether its outbox should be indexed.
575    fn is_tracked(&self, chain_id: &ChainId) -> bool {
576        self.chain_modes.as_ref().is_none_or(|chain_modes| {
577            chain_modes
578                .read()
579                .expect("Panics should not happen while holding a lock to `chain_modes`")
580                .get(chain_id)
581                .is_some_and(ListeningMode::is_full)
582        })
583    }
584
585    /// Reconciles the chain's `nonempty_outboxes` and `outbox_counters` indices with the current
586    /// set of fully-tracked chains.
587    async fn reconcile_tracked_outboxes(
588        &mut self,
589    ) -> Result<Option<Arc<Hashed<ChainIdSet>>>, WorkerError> {
590        let full_chains = self.tracked_full_chains();
591        self.chain
592            .reconcile_outbox_index(full_chains.as_deref())
593            .await?;
594        Ok(full_chains)
595    }
596
597    /// Reconciles the outbox index with the current tracked set, then loads pending cross-chain
598    /// requests and adds `NewRound` notifications where appropriate.
599    async fn create_network_actions(
600        &mut self,
601        old_round: Option<Round>,
602    ) -> Result<NetworkActions, WorkerError> {
603        // Make the outbox index authoritative for the current tracked set first, so it already
604        // holds only `is_full` targets and needs no read-time filtering.
605        let tracked = self.reconcile_tracked_outboxes().await?;
606        self.build_network_actions(old_round, tracked.as_deref().map(|h| h.inner()))
607            .await
608    }
609
610    /// Builds the pending cross-chain actions from the already-reconciled outbox index.
611    async fn build_network_actions(
612        &self,
613        old_round: Option<Round>,
614        tracked: Option<&ChainIdSet>,
615    ) -> Result<NetworkActions, WorkerError> {
616        #[cfg(with_metrics)]
617        let _latency = metrics::CREATE_NETWORK_ACTIONS_LATENCY.measure_latency();
618        let mut heights_by_recipient = BTreeMap::<_, Vec<_>>::new();
619        let targets = self.chain.nonempty_outbox_chain_ids();
620        if let Some(tracked) = tracked {
621            if let Some(target) = targets.iter().find(|target| !tracked.contains(*target)) {
622                return Err(ChainError::CorruptedChainState(format!(
623                    "outbox index contains untracked target {target}"
624                ))
625                .into());
626            }
627        }
628        let outboxes = self.chain.load_outboxes(&targets).await?;
629        for (target, outbox) in targets.into_iter().zip(outboxes) {
630            let heights = outbox.queue.elements().await?;
631            heights_by_recipient.insert(target, heights);
632        }
633        let cross_chain_requests = self
634            .create_cross_chain_requests(heights_by_recipient)
635            .await?;
636        let mut notifications = Vec::new();
637        if let Some(old_round) = old_round {
638            let round = self.chain.manager.current_round();
639            if round > old_round {
640                let height = self.chain.tip_state.get().next_block_height;
641                notifications.push(Notification {
642                    chain_id: self.chain_id(),
643                    reason: Reason::NewRound { height, round },
644                });
645            }
646        }
647        Ok(NetworkActions {
648            cross_chain_requests,
649            notifications,
650        })
651    }
652
653    /// Returns confirmed blocks by hash, checking the cache first and batch-loading the rest
654    /// from storage. The order of the returned blocks matches the order of the input hashes.
655    async fn read_confirmed_blocks(
656        &self,
657        hashes: &[CryptoHash],
658    ) -> Result<Vec<Option<CacheArc<ConfirmedBlock>>>, WorkerError> {
659        let mut blocks = Vec::with_capacity(hashes.len());
660        let mut uncached_indices = Vec::new();
661        let mut uncached_hashes = Vec::new();
662
663        for (i, hash) in hashes.iter().enumerate() {
664            if let Some(block) = self.block_values.get(hash) {
665                blocks.push(Some(block));
666            } else {
667                blocks.push(None);
668                uncached_indices.push(i);
669                uncached_hashes.push(*hash);
670            }
671        }
672
673        if !uncached_hashes.is_empty() {
674            let from_storage = self.storage.read_confirmed_blocks(uncached_hashes).await?;
675            for (i, maybe_block) in uncached_indices.into_iter().zip(from_storage) {
676                blocks[i] = maybe_block;
677            }
678        }
679
680        Ok(blocks)
681    }
682
683    #[instrument(skip_all, fields(
684        chain_id = %self.chain_id(),
685        num_recipients = %heights_by_recipient.len()
686    ))]
687    async fn create_cross_chain_requests(
688        &self,
689        heights_by_recipient: BTreeMap<ChainId, Vec<BlockHeight>>,
690    ) -> Result<Vec<CrossChainRequest>, WorkerError> {
691        // Load all the certificates we will need, regardless of the medium.
692        let heights = heights_by_recipient
693            .values()
694            .flatten()
695            .copied()
696            .collect::<BTreeSet<_>>();
697        let hashes = self
698            .chain
699            .block_hashes_for_heights(heights.iter().copied())
700            .await?;
701
702        let blocks = self.read_confirmed_blocks(&hashes).await?;
703
704        let mut height_to_blocks = HashMap::new();
705        for (block, hash) in blocks.into_iter().zip(hashes) {
706            let block = block.ok_or_else(|| WorkerError::ReadCertificatesError(vec![hash]))?;
707            height_to_blocks.insert(block.height(), block);
708        }
709
710        let sender = self.chain.chain_id();
711        let mut cross_chain_requests = Vec::new();
712        for (recipient, heights) in heights_by_recipient {
713            // Extract the predecessor height for this recipient from the first
714            // block's `previous_message_blocks`. This lets the recipient detect
715            // gaps even before it consumes the missing message.
716            let previous_height = heights.first().and_then(|first_height| {
717                let block = height_to_blocks.get(first_height)?;
718                let (_, prev_height) =
719                    block.block().body.previous_message_blocks.get(&recipient)?;
720                Some(*prev_height)
721            });
722            let mut bundles = Vec::new();
723            let mut bundles_size = 0;
724            for height in heights {
725                let Some(confirmed_block) = height_to_blocks.get(&height) else {
726                    tracing::warn!(
727                        %height,
728                        %recipient,
729                        "spurious entry in outbox; skipping this and higher sender blocks"
730                    );
731                    break;
732                };
733                let new_bundles = confirmed_block
734                    .block()
735                    .message_bundles_for(recipient, confirmed_block.inner().hash())
736                    .collect::<Vec<_>>();
737                let new_size = new_bundles
738                    .iter()
739                    .map(|(_epoch, bundle)| bundle.estimated_size())
740                    .sum::<usize>();
741                // If adding this block's bundles would exceed the chunk limit,
742                // stop here. Always include at least one block's bundles.
743                if bundles_size + new_size > self.config.cross_chain_message_chunk_limit {
744                    if bundles.is_empty() {
745                        warn!(
746                            "Single block at height {height} produces an UpdateRecipient \
747                            of ~{new_size} bytes, exceeding the chunk limit of {}",
748                            self.config.cross_chain_message_chunk_limit
749                        );
750                    } else {
751                        debug!(
752                            "Stopping cross-chain batch for {recipient} at height {height}: \
753                            adding ~{new_size} bytes would exceed chunk limit of {} \
754                            (current batch ~{bundles_size} bytes)",
755                            self.config.cross_chain_message_chunk_limit
756                        );
757                        break;
758                    }
759                }
760                bundles.extend(new_bundles);
761                bundles_size += new_size;
762            }
763            if !bundles.is_empty() {
764                cross_chain_requests.push(CrossChainRequest::UpdateRecipient {
765                    sender,
766                    recipient,
767                    bundles,
768                    previous_height,
769                });
770            }
771        }
772        Ok(cross_chain_requests)
773    }
774
775    /// Processes a leader timeout issued for this multi-owner chain.
776    #[instrument(skip_all, fields(
777        chain_id = %self.chain_id(),
778        height = %certificate.inner().height()
779    ))]
780    pub(crate) async fn process_timeout(
781        &mut self,
782        certificate: TimeoutCertificate,
783    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
784        // Check that the chain is active and ready for this timeout.
785        // Verify the certificate. Returns a catch-all error to make client code more robust.
786        self.initialize_and_save_if_needed().await?;
787        let (chain_epoch, committee) = self.chain.current_committee().await?;
788        certificate.check(&committee)?;
789        if self
790            .chain
791            .tip_state
792            .get()
793            .already_validated_block(certificate.inner().height())?
794        {
795            return Ok((self.chain_info_response().await?, NetworkActions::default()));
796        }
797        ensure!(
798            certificate.inner().epoch() == chain_epoch,
799            WorkerError::InvalidEpoch {
800                chain_id: certificate.inner().chain_id(),
801                chain_epoch,
802                epoch: certificate.inner().epoch()
803            }
804        );
805        let old_round = self.chain.manager.current_round();
806        self.chain
807            .manager
808            .handle_timeout_certificate(certificate, self.storage.clock().current_time());
809        self.save().await?;
810        let actions = self.create_network_actions(Some(old_round)).await?;
811        Ok((self.chain_info_response().await?, actions))
812    }
813
814    /// Tries to load all blobs published in this proposal.
815    ///
816    /// If they cannot be found, it creates an entry in `pending_proposed_blobs` so they can be
817    /// submitted one by one.
818    #[instrument(skip_all, fields(
819        chain_id = %self.chain_id(),
820        block_height = %proposal.content.block.height
821    ))]
822    async fn load_proposal_blobs(
823        &mut self,
824        proposal: &BlockProposal,
825    ) -> Result<Vec<Blob>, WorkerError> {
826        let owner = proposal.owner();
827        let BlockProposal {
828            content:
829                ProposalContent {
830                    block,
831                    round,
832                    outcome: _,
833                },
834            original_proposal,
835            signature: _,
836        } = proposal;
837
838        let mut maybe_blobs = self
839            .maybe_get_required_blobs(proposal.required_blob_ids(), None)
840            .await?;
841        let missing_blob_ids = missing_blob_ids(&maybe_blobs);
842        if !missing_blob_ids.is_empty() {
843            let chain = &mut self.chain;
844            if chain.ownership().await?.open_multi_leader_rounds {
845                // TODO(#3203): Allow multiple pending proposals on permissionless chains.
846                chain.pending_proposed_blobs.clear();
847            }
848            let validated = matches!(original_proposal, Some(OriginalProposal::Regular { .. }));
849            chain
850                .pending_proposed_blobs
851                .try_load_entry_mut(&owner)
852                .await?
853                .update(*round, validated, maybe_blobs)?;
854            self.save().await?;
855            return Err(WorkerError::BlobsNotFound(missing_blob_ids));
856        }
857        let published_blobs = block
858            .published_blob_ids()
859            .iter()
860            .filter_map(|blob_id| maybe_blobs.remove(blob_id).flatten())
861            .collect::<Vec<_>>();
862        Ok(published_blobs)
863    }
864
865    /// Processes a validated block issued for this multi-owner chain.
866    #[instrument(skip_all, fields(
867        chain_id = %self.chain_id(),
868        block_height = %certificate.block().header.height
869    ))]
870    pub(crate) async fn process_validated_block(
871        &mut self,
872        certificate: ValidatedBlockCertificate,
873    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
874        let block = certificate.block();
875
876        let header = &block.header;
877        let height = header.height;
878        // Check that the chain is active and ready for this validated block.
879        // Verify the certificate. Returns a catch-all error to make client code more robust.
880        self.initialize_and_save_if_needed().await?;
881        let tip_state = self.chain.tip_state.get();
882        ensure!(
883            header.height == tip_state.next_block_height,
884            ChainError::UnexpectedBlockHeight {
885                expected_block_height: tip_state.next_block_height,
886                found_block_height: header.height,
887            }
888        );
889        let (epoch, committee) = self.chain.current_committee().await?;
890        check_block_epoch(epoch, header.chain_id, header.epoch)?;
891        certificate.check(&committee)?;
892        let already_committed_block = self.chain.tip_state.get().already_validated_block(height)?;
893        let should_skip_validated_block = || {
894            self.chain
895                .manager
896                .check_validated_block(&certificate)
897                .map(|outcome| outcome == manager::Outcome::Skip)
898        };
899        if already_committed_block || should_skip_validated_block()? {
900            // If we just processed the same pending block, return the chain info unchanged.
901            return Ok((
902                self.chain_info_response().await?,
903                NetworkActions::default(),
904                BlockOutcome::Skipped,
905            ));
906        }
907
908        self.block_values
909            .insert_hashed(Cow::Borrowed(certificate.inner().inner()));
910        let required_blob_ids = block.required_blob_ids();
911        let maybe_blobs = self
912            .maybe_get_required_blobs(required_blob_ids, Some(block.created_blobs()))
913            .await?;
914        let missing_blob_ids = missing_blob_ids(&maybe_blobs);
915        if !missing_blob_ids.is_empty() {
916            self.chain
917                .pending_validated_blobs
918                .update(certificate.round, true, maybe_blobs)?;
919            self.save().await?;
920            return Err(WorkerError::BlobsNotFound(missing_blob_ids));
921        }
922        let blobs = maybe_blobs
923            .into_iter()
924            .filter_map(|(blob_id, maybe_blob)| Some((blob_id, maybe_blob?)))
925            .collect();
926        let old_round = self.chain.manager.current_round();
927        self.chain.manager.create_final_vote(
928            certificate,
929            self.config.key_pair(),
930            self.storage.clock().current_time(),
931            blobs,
932        )?;
933        self.save().await?;
934        let actions = self.create_network_actions(Some(old_round)).await?;
935        Ok((
936            self.chain_info_response().await?,
937            actions,
938            BlockOutcome::Processed,
939        ))
940    }
941
942    /// Processes a confirmed block (aka a commit).
943    #[instrument(skip_all, fields(
944        chain_id = %certificate.block().header.chain_id,
945        height = %certificate.block().header.height,
946        block_hash = %certificate.hash(),
947    ))]
948    pub(crate) async fn process_confirmed_block(
949        &mut self,
950        certificate: ConfirmedBlockCertificate,
951        mode: ProcessConfirmedBlockMode,
952        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
953    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
954        let block = certificate.block();
955        let block_hash = certificate.hash();
956        let height = block.header.height;
957        let chain_id = block.header.chain_id;
958
959        // Trust-mark accept path: an earlier checkpoint cert recorded this block's
960        // hash in `pre_checkpoint_block_trust`. Removing the trust mark here is
961        // only an in-memory change; if anything below fails before `save`, the
962        // mark survives on the next reload. The dispatch's `gap` definition
963        // (`!=` rather than `<`) routes both above-tip and below-tip trust
964        // uploads to `preprocess_certified_block` so the chain can write the
965        // cert without advancing the tip.
966        let in_trust_set = self
967            .chain
968            .pre_checkpoint_block_trust
969            .contains(&block_hash)
970            .await?;
971        if in_trust_set {
972            self.chain.pre_checkpoint_block_trust.remove(&block_hash)?;
973        }
974
975        // Check if we already processed this block.
976        let tip = self.chain.tip_state.get().clone();
977        if !in_trust_set && tip.next_block_height > height {
978            let actions = self.create_network_actions(None).await?;
979            self.register_delivery_notifier(height, &actions, notify_when_messages_are_delivered)
980                .await;
981            return Ok((
982                self.chain_info_response().await?,
983                actions,
984                BlockOutcome::Skipped,
985            ));
986        }
987
988        // We haven't processed the block - verify the certificate first.
989        let committee = self.committee_for_epoch(block.header.epoch).await?;
990        certificate.check(&committee)?;
991
992        // Certificate check passed - which means the blobs the block requires are legitimate and
993        // we can take note of it, so that if any are missing, we will accept them when the client
994        // sends them.
995        let required_blob_ids = block.required_blob_ids();
996        let blobs_result = self
997            .get_required_blobs(required_blob_ids.iter().copied(), block.created_blobs())
998            .await
999            .map(|blobs| blobs.into_values().collect::<Vec<_>>());
1000
1001        if let Ok(blobs) = &blobs_result {
1002            self.storage
1003                .write_blobs_and_certificate(blobs, &certificate)
1004                .await?;
1005            let events = block
1006                .body
1007                .events
1008                .iter()
1009                .flatten()
1010                .map(|event| (event.id(chain_id), event.value.clone()));
1011            self.storage.write_events(events).await?;
1012        }
1013
1014        // Update the blob state with last used certificate hash.
1015        let blob_state = certificate.value().to_blob_state(blobs_result.is_ok());
1016        let blob_ids = required_blob_ids.into_iter().collect::<Vec<_>>();
1017        self.storage
1018            .maybe_write_blob_states(&blob_ids, blob_state)
1019            .await?;
1020
1021        let blobs = blobs_result?
1022            .into_iter()
1023            .map(|blob| (blob.id(), blob))
1024            .collect::<BTreeMap<_, _>>();
1025
1026        // Dispatch on the actual outcome (preprocess / checkpoint-restore-then-execute
1027        // / contiguous execute):
1028        //  - `Preprocess` mode, or `Auto` with an unbridgeable gap: preprocess.
1029        //  - `Execute` mode with an unbridgeable gap: error.
1030        //  - `Auto`/`Execute` mode + gap + checkpoint block: install the snapshot,
1031        //    then execute.
1032        //  - `Auto`/`Execute` mode + contiguous: execute directly.
1033        use ProcessConfirmedBlockMode::{Auto, Execute, Preprocess};
1034        let gap = tip.next_block_height != height;
1035        let starts_with_checkpoint = block.starts_with_checkpoint();
1036        match (mode, gap, starts_with_checkpoint) {
1037            (Preprocess, _, _) | (Auto, true, false) => {
1038                self.preprocess_certified_block(certificate, notify_when_messages_are_delivered)
1039                    .await
1040            }
1041            (Execute, true, false) => Err(WorkerError::InvalidBlockChaining),
1042            (Auto | Execute, true, true) => {
1043                self.execute_block_with_checkpoint_restore(
1044                    certificate,
1045                    blobs,
1046                    notify_when_messages_are_delivered,
1047                )
1048                .await
1049            }
1050            (Auto | Execute, false, _) => {
1051                self.execute_contiguous_block(
1052                    certificate,
1053                    blobs,
1054                    tip,
1055                    notify_when_messages_are_delivered,
1056                )
1057                .await
1058            }
1059        }
1060    }
1061
1062    /// Preprocesses a confirmed block: updates outboxes and event streams without
1063    /// executing it, and does not advance the chain tip.
1064    async fn preprocess_certified_block(
1065        &mut self,
1066        certificate: ConfirmedBlockCertificate,
1067        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
1068    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
1069        let block_hash = certificate.hash();
1070        let block = certificate.block();
1071        let chain_id = block.header.chain_id;
1072        let height = block.header.height;
1073
1074        let tracked = self.reconcile_tracked_outboxes().await?;
1075        let updated_event_streams = self
1076            .chain
1077            .preprocess_block(certificate.value(), tracked.as_deref().map(|h| h.inner()))
1078            .await?;
1079        self.save().await?;
1080        let mut actions = self.create_network_actions(None).await?;
1081        if !updated_event_streams.is_empty() {
1082            actions.notifications.push(Notification {
1083                chain_id,
1084                reason: Reason::NewEvents {
1085                    height,
1086                    block_hash,
1087                    event_streams: updated_event_streams,
1088                },
1089            });
1090        }
1091        trace!("Preprocessed confirmed block {height}");
1092        self.register_delivery_notifier(height, &actions, notify_when_messages_are_delivered)
1093            .await;
1094        Ok((
1095            self.chain_info_response().await?,
1096            actions,
1097            BlockOutcome::Preprocessed,
1098        ))
1099    }
1100
1101    /// Restores the chain's execution state from the checkpoint blob embedded in
1102    /// the block's first oracle response, fast-forwards the tip to the block's
1103    /// height, then executes the block as if it were contiguous. Re-running the
1104    /// block applies its fees and re-derives any post-checkpoint state.
1105    async fn execute_block_with_checkpoint_restore(
1106        &mut self,
1107        certificate: ConfirmedBlockCertificate,
1108        blobs: BTreeMap<BlobId, Blob>,
1109        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
1110    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
1111        let (bytes, chain_id, height, previous_block_hash, outbox_block_hashes, inbox_cursors) = {
1112            let block = certificate.block();
1113            let Some(OracleResponse::Checkpoint {
1114                execution_state_blobs,
1115                outbox_block_hashes,
1116                inbox_cursors,
1117                ..
1118            }) = block.body.oracle_responses.first().and_then(|r| r.first())
1119            else {
1120                return Err(ChainError::InternalError(
1121                    "Checkpoint block missing OracleResponse::Checkpoint".into(),
1122                )
1123                .into());
1124            };
1125            let mut bytes = Vec::new();
1126            let mut missing = Vec::new();
1127            for hash in execution_state_blobs {
1128                let blob_id = BlobId::new(*hash, BlobType::CheckpointExecutionState);
1129                match blobs.get(&blob_id) {
1130                    Some(blob) => bytes.extend_from_slice(blob.bytes()),
1131                    None => missing.push(blob_id),
1132                }
1133            }
1134            ensure!(missing.is_empty(), WorkerError::BlobsNotFound(missing));
1135            (
1136                bytes,
1137                block.header.chain_id,
1138                block.header.height,
1139                block.header.previous_block_hash,
1140                outbox_block_hashes.clone(),
1141                inbox_cursors.clone(),
1142            )
1143        };
1144        // Every pre-checkpoint sender block the oracle response names must already
1145        // be in storage before we touch any chain state. If some aren't, record
1146        // their hashes as trusted-by-this-checkpoint and error with `BlocksNotFound`
1147        // — the client uploads each missing cert (the trust-mark path at the top
1148        // of `process_confirmed_block` accepts them regardless of their possibly
1149        // revoked epoch), then retries the checkpoint. This way the worker never
1150        // exposes a half-restored chain whose outboxes reference unknown blocks.
1151        let mut missing_blocks = Vec::new();
1152        for hash in &outbox_block_hashes {
1153            if !self.storage.contains_certificate(*hash).await? {
1154                missing_blocks.push(*hash);
1155            }
1156        }
1157        if !missing_blocks.is_empty() {
1158            for hash in &missing_blocks {
1159                self.chain.pre_checkpoint_block_trust.insert(hash)?;
1160            }
1161            self.save().await?;
1162            return Err(WorkerError::BlocksNotFound(missing_blocks));
1163        }
1164        self.chain
1165            .execution_state
1166            .restore_from_content(&bytes)
1167            .await?;
1168        // `restore_from_content` writes directly to storage and leaves the
1169        // in-memory view in an undefined state — reload from storage.
1170        self.chain = self.storage.load_chain(chain_id).await?;
1171        // Re-populate `block_hashes` for every pre-checkpoint sender block the
1172        // chain still needs. The heights live in the just-restored execution
1173        // state (`unfinalized_message_blocks`); the matching hashes are the
1174        // ones the producer recorded in the oracle response, certified by the
1175        // checkpoint cert we already trust. Without this, the next step
1176        // (re-executing the checkpoint to verify its outcome) would fail
1177        // because `collect_unfinalized_block_hashes` looks these up.
1178        let heights = self.chain.collect_unfinalized_heights().await?;
1179        ensure!(
1180            heights.len() == outbox_block_hashes.len(),
1181            ChainError::InternalError(format!(
1182                "checkpoint oracle response has {} outbox block hashes but the \
1183                 restored state references {} distinct heights",
1184                outbox_block_hashes.len(),
1185                heights.len(),
1186            ))
1187        );
1188        for (height, hash) in heights.into_iter().zip(outbox_block_hashes) {
1189            self.chain.block_hashes.insert(&height, hash)?;
1190        }
1191        // Rebuild the off-chain outbox state (queues, counters,
1192        // nonempty_outboxes) from the on-chain unfinalized map so that this
1193        // node can resume pushing pre-checkpoint messages forward. The outbox
1194        // isn't part of the certified checkpoint blob, so without this a
1195        // bootstrapped validator would silently stop delivering pending
1196        // messages.
1197        let tracked = self.tracked_full_chains();
1198        self.chain
1199            .restore_outboxes_from_unfinalized(tracked.as_deref())
1200            .await?;
1201        for (origin, cursor) in inbox_cursors {
1202            let mut inbox = self.chain.inboxes.try_load_entry_mut(&origin).await?;
1203            inbox.restore_from_checkpoint(cursor).await?;
1204        }
1205        // Seed `next_expected_events` from the restored per-stream event counts. Re-executing
1206        // the checkpoint re-emits each summarized stream's summary event; without this seed
1207        // the summary's index would look like a gap on the freshly-restored node, so the
1208        // tracker would never advance and future events on that stream would never be
1209        // delivered to subscribers. The counts are contiguous, so this matches the producer's
1210        // tracker as of just before the checkpoint block.
1211        for (stream_id, count) in self
1212            .chain
1213            .execution_state
1214            .system
1215            .stream_event_counts
1216            .index_values()
1217            .await?
1218        {
1219            // Just after restoring, every event predates the checkpoint, so the readable floor
1220            // is the count itself.
1221            self.chain.next_expected_events.insert(
1222                &stream_id,
1223                StreamCounts {
1224                    first_index: count,
1225                    next_index: count,
1226                },
1227            )?;
1228        }
1229        // We reset `execution_state` (via restore), `tip_state`, `block_hashes`
1230        // (for outbox-referenced pre-checkpoint heights), and the outbox views.
1231        // The other `ChainStateView` fields are either (a) already default for
1232        // a fresh bootstrap node (`inboxes`, `received_log`, …), (b) about to
1233        // be overwritten by `apply_confirmed_block` when the cert is applied
1234        // (`manager`, `block_hashes` for height `height`), or (c) outside the
1235        // protocol state hash so divergence from the producer is fine
1236        // (inboxes; subsequent blocks reconcile by anticipation if needed).
1237        let new_tip = ChainTipState {
1238            block_hash: previous_block_hash,
1239            next_block_height: height,
1240        };
1241        self.chain.tip_state.set(new_tip.clone());
1242        // Installing a snapshot establishes the chain's state from scratch (block 0 is never
1243        // executed here), so record it as the initialization time for the reset cooldown.
1244        self.chain
1245            .chain_initialized_at
1246            .set(self.storage.clock().current_time());
1247        self.save().await?;
1248        self.execute_contiguous_block(
1249            certificate,
1250            blobs,
1251            new_tip,
1252            notify_when_messages_are_delivered,
1253        )
1254        .await
1255    }
1256
1257    /// Executes a confirmed block whose height equals `tip.next_block_height`,
1258    /// updating inboxes, applying the block, and persisting the chain.
1259    async fn execute_contiguous_block(
1260        &mut self,
1261        certificate: ConfirmedBlockCertificate,
1262        mut blobs: BTreeMap<BlobId, Blob>,
1263        tip: ChainTipState,
1264        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
1265    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
1266        // Cached only when export is on: the queue holds the shared pointer, and inserting on
1267        // every executed block would otherwise churn the dedup cache for nothing.
1268        let (cached, plain) = if self.block_export.is_some() {
1269            (Some(self.storage.cache_certificate(certificate)), None)
1270        } else {
1271            (None, Some(certificate))
1272        };
1273        let certificate = cached
1274            .as_deref()
1275            .or(plain.as_ref())
1276            .expect("exactly one of the two is set");
1277        let block_hash = certificate.hash();
1278        let block = certificate.block();
1279        let chain_id = block.header.chain_id;
1280        let height = block.header.height;
1281
1282        // This should always be true for valid certificates.
1283        ensure!(
1284            tip.block_hash == block.header.previous_block_hash,
1285            WorkerError::InvalidBlockChaining
1286        );
1287
1288        // Verify that the chain is active and that the epoch we used for verifying
1289        // the certificate is actually the active one on the chain.
1290        self.initialize_and_save_if_needed().await?;
1291        let (epoch, _) = self.chain.current_committee().await?;
1292        check_block_epoch(epoch, chain_id, block.header.epoch)?;
1293
1294        // The chain is initialized and this block has not executed yet, so the current ownership
1295        // is the configuration the block was proposed under — even for the chain's first block,
1296        // whose ownership comes from the just-applied chain description. This is the point where
1297        // the first-round attestation can be checked against the actual first round; blocks that
1298        // are only preprocessed skip it and rely on the nodes that execute the chain in order.
1299        if certificate.first_round() {
1300            ensure!(
1301                certificate.round() == self.chain.ownership().await?.first_round(),
1302                ChainError::FalseFirstRoundAttestation
1303            );
1304        }
1305
1306        let published_blobs = block
1307            .published_blob_ids()
1308            .iter()
1309            .filter_map(|blob_id| blobs.remove(blob_id))
1310            .collect::<Vec<_>>();
1311
1312        let local_time = self.storage.clock().current_time();
1313        if block.header.timestamp.duration_since(local_time) > self.config.block_time_grace_period {
1314            warn!(
1315                block_timestamp = %block.header.timestamp,
1316                %local_time,
1317                "Confirmed block has a timestamp in the future beyond the block time grace period"
1318            );
1319        }
1320        let tracked = self.reconcile_tracked_outboxes().await?;
1321        let chain = &mut self.chain;
1322        chain
1323            .remove_bundles_from_inboxes(
1324                block.header.timestamp,
1325                false,
1326                block.body.incoming_bundles(),
1327            )
1328            .await?;
1329        let confirmed_block = if let Some(mut execution_state) = self
1330            .execution_state_cache
1331            .as_ref()
1332            .and_then(|cache| cache.remove(&block_hash))
1333        {
1334            chain.execution_state = execution_state
1335                .with_context(|ctx| {
1336                    chain
1337                        .execution_state
1338                        .context()
1339                        .clone_with_base_key(ctx.base_key().bytes.clone())
1340                })
1341                .await;
1342            Cow::Borrowed(certificate.value())
1343        } else {
1344            let (proposed_block, outcome) = block.clone().into_proposal();
1345            let (proposed_block, verified, _resource_tracker, _) = chain
1346                .execute_block(
1347                    proposed_block,
1348                    local_time,
1349                    None,
1350                    &published_blobs,
1351                    BlockExecution::HandleConfirmed {
1352                        oracle_responses: outcome.oracle_responses.clone(),
1353                    },
1354                )
1355                .await?;
1356            // We should always agree on the messages and state hash.
1357            if outcome != verified {
1358                return Err(ChainError::CorruptedChainState(format!(
1359                    "computed block outcome differs from the certificate.\n\
1360                    Computed: {verified:#?}\n\
1361                    Submitted: {outcome:#?}"
1362                ))
1363                .into());
1364            }
1365            Cow::Owned(ConfirmedBlock::new(Block::new(proposed_block, verified)))
1366        };
1367
1368        let updated_streams = chain
1369            .apply_confirmed_block(
1370                &confirmed_block,
1371                local_time,
1372                tracked.as_deref().map(|h| h.inner()),
1373            )
1374            .await?;
1375        self.export_block(cached.as_ref(), published_blobs, blobs)
1376            .await;
1377        let mut actions = self.create_network_actions(None).await?;
1378        trace!("Processed confirmed block {height}");
1379        actions.notifications.push(Notification {
1380            chain_id,
1381            reason: Reason::NewBlock {
1382                height,
1383                hash: block_hash,
1384            },
1385        });
1386        if !updated_streams.is_empty() {
1387            actions.notifications.push(Notification {
1388                chain_id,
1389                reason: Reason::NewEvents {
1390                    height,
1391                    block_hash,
1392                    event_streams: updated_streams,
1393                },
1394            });
1395        }
1396        self.save().await?;
1397
1398        self.block_values.insert_hashed(match confirmed_block {
1399            Cow::Borrowed(block) => Cow::Borrowed(block.inner()),
1400            Cow::Owned(block) => Cow::Owned(block.into_inner()),
1401        });
1402
1403        self.register_delivery_notifier(height, &actions, notify_when_messages_are_delivered)
1404            .await;
1405
1406        Ok((
1407            self.chain_info_response().await?,
1408            actions,
1409            BlockOutcome::Processed,
1410        ))
1411    }
1412
1413    /// Queues the block just executed for export and folds the queue's progress into the chain
1414    /// state, for the save that follows to persist. Returns as soon as it is queued, and does
1415    /// nothing if export is disabled. Never fails the block: export is replication, not
1416    /// consensus, so a problem here is logged and the block stands.
1417    async fn export_block(
1418        &mut self,
1419        certificate: Option<&CacheArc<ConfirmedBlockCertificate>>,
1420        published_blobs: Vec<Blob>,
1421        read_blobs: BTreeMap<BlobId, Blob>,
1422    ) {
1423        // The certificate is cached exactly when export is enabled, so both are `Some` or
1424        // neither is.
1425        let (Some(export), Some(certificate)) = (self.block_export.clone(), certificate) else {
1426            return;
1427        };
1428        // The committee *after* applying the block: a validator that this very block admits has to
1429        // be exported to from now on.
1430        let (epoch, committee) = match self.chain.current_committee().await {
1431            Ok((epoch, committee)) => (epoch, committee),
1432            Err(error) => {
1433                warn!(%error, "Not exporting a block of a chain with no current committee");
1434                return;
1435            }
1436        };
1437
1438        // Through the cache rather than `Arc::new`: these blobs are already in storage's cache
1439        // in the common case, and the cache is what keeps one allocation per blob content.
1440        let blobs = published_blobs
1441            .into_iter()
1442            .chain(read_blobs.into_values())
1443            .map(|blob| self.storage.cache_blob(blob))
1444            .collect();
1445        export.export(
1446            certificate.clone(),
1447            blobs,
1448            epoch,
1449            (**self.chain.exported_heights.get()).clone(),
1450        );
1451
1452        // Fold in what the queue has recorded so far — which never includes the block we just
1453        // queued. Merged by maximum: the queue drops a chain's progress once it converges, and an
1454        // empty read must not regress what an earlier save persisted.
1455        let acknowledged = export.progress(self.chain.chain_id(), &committee);
1456        let mut merged = std::collections::BTreeMap::new();
1457        for validator in committee.validators().keys() {
1458            let previous = self.chain.exported_heights.get().get(validator).copied();
1459            let reported = acknowledged.get(validator).copied();
1460            if let Some(height) = previous.max(reported) {
1461                merged.insert(*validator, height);
1462            }
1463        }
1464        if **self.chain.exported_heights.get() != merged {
1465            // Rewrites of an already-populated register are throttled: it is re-serialized
1466            // whole, changes on every block of an active chain, and is a lower bound by design.
1467            // The first real content is never held back. The tail of a burst may therefore
1468            // never be folded at all — there is no later save to carry it — which is accepted:
1469            // the cost is one query per destination when the cursor is rebuilt, and the
1470            // alternative is a full register write per block.
1471            let now = linera_base::time::Instant::now();
1472            let throttled = !self.chain.exported_heights.get().is_empty()
1473                && self.last_exported_heights_fold.is_some_and(|last| {
1474                    now.duration_since(last) < self.config.exported_heights_fold_interval
1475                });
1476            if !throttled {
1477                self.last_exported_heights_fold = Some(now);
1478                self.chain.exported_heights.set(merged.into());
1479            }
1480        }
1481    }
1482
1483    /// Schedules a notification for when cross-chain messages are delivered up to the given
1484    /// `height`.
1485    #[instrument(level = "trace", skip(self, notify_when_messages_are_delivered))]
1486    async fn register_delivery_notifier(
1487        &self,
1488        height: BlockHeight,
1489        actions: &NetworkActions,
1490        notify_when_messages_are_delivered: Option<oneshot::Sender<()>>,
1491    ) {
1492        if let Some(notifier) = notify_when_messages_are_delivered {
1493            if actions
1494                .cross_chain_requests
1495                .iter()
1496                .any(|request| request.has_messages_lower_or_equal_than(height))
1497            {
1498                self.delivery_notifier.register(height, notifier);
1499            } else {
1500                // No need to wait. Also, cross-chain requests may not trigger the
1501                // notifier later, even if we register it.
1502                if let Err(()) = notifier.send(()) {
1503                    debug!("Failed to notify message delivery to caller (early case)");
1504                }
1505            }
1506        }
1507    }
1508
1509    /// Updates the chain's inboxes, receiving messages from a cross-chain update.
1510    #[instrument(level = "debug", skip(self, bundles), fields(chain_id = %self.chain_id()))]
1511    pub(crate) async fn process_cross_chain_update(
1512        &mut self,
1513        origin: ChainId,
1514        bundles: Vec<(Epoch, MessageBundle)>,
1515        sender_previous_height: Option<BlockHeight>,
1516    ) -> Result<CrossChainUpdateResult, WorkerError> {
1517        // Only process certificates with relevant heights and epochs.
1518        let mut inbox = self.chain.inboxes.try_load_entry_mut(&origin).await?;
1519        let next_height_to_receive = inbox.next_block_height_to_receive()?;
1520        let last_anticipated_block_height = inbox
1521            .removed_bundles
1522            .back()
1523            .await?
1524            .map(|bundle| bundle.height);
1525
1526        // Proactive gap detection: if the sender declares a predecessor height that
1527        // we haven't received yet, the inbox has a gap.
1528        if let Some(prev) = sender_previous_height {
1529            if prev >= next_height_to_receive {
1530                let chain_id = self.chain_id();
1531                if self.config.allow_revert_confirm && self.config.recovery_allowed_for(&chain_id) {
1532                    warn!(
1533                        %chain_id,
1534                        "Inbox gap detected from {origin}: \
1535                        sender declares previous height {prev} but we only have up to \
1536                        {next_height_to_receive}; requesting resend",
1537                    );
1538                    return Ok(CrossChainUpdateResult::GapDetected {
1539                        origin,
1540                        retransmit_from: next_height_to_receive,
1541                    });
1542                }
1543                return Err(ChainError::InboxGapDetected {
1544                    chain_id,
1545                    origin,
1546                    expected_height: prev,
1547                    actual_height: bundles.first().map(|(_, b)| b.height).unwrap_or_default(),
1548                }
1549                .into());
1550            }
1551        }
1552
1553        let bundles = self
1554            .select_message_bundles(
1555                &origin,
1556                next_height_to_receive,
1557                last_anticipated_block_height,
1558                bundles,
1559            )
1560            .await?;
1561        let Some(last_updated_height) = bundles.last().map(|bundle| bundle.height) else {
1562            return Ok(CrossChainUpdateResult::NothingToDo);
1563        };
1564        // Process the received messages in certificates.
1565        let local_time = self.storage.clock().current_time();
1566        let mut previous_height = None;
1567        for bundle in bundles {
1568            let add_to_received_log = previous_height != Some(bundle.height);
1569            previous_height = Some(bundle.height);
1570            // Update the staged chain state with the received block.
1571            self.chain
1572                .receive_message_bundle_with_inbox(
1573                    &mut inbox,
1574                    &origin,
1575                    bundle,
1576                    local_time,
1577                    add_to_received_log,
1578                )
1579                .await?;
1580        }
1581        inbox.observe_size_metric();
1582        drop(inbox);
1583        if !self.config.allow_inactive_chains && !self.chain.is_active().await? {
1584            // Refuse to create a chain state if the chain is still inactive by
1585            // now. Accordingly, do not send a confirmation, so that the
1586            // cross-chain update is retried later.
1587            warn!(
1588                chain_id = %self.chain_id(),
1589                "Refusing to deliver messages from {origin} \
1590                at height {last_updated_height} because the recipient is still inactive",
1591            );
1592            return Ok(CrossChainUpdateResult::NothingToDo);
1593        }
1594        Ok(CrossChainUpdateResult::Updated(last_updated_height))
1595    }
1596
1597    /// Handles the cross-chain request confirming that the recipient was updated.
1598    #[instrument(skip_all, fields(
1599        chain_id = %self.chain_id(),
1600        %recipient,
1601        %latest_height
1602    ))]
1603    pub(crate) async fn confirm_updated_recipient(
1604        &mut self,
1605        recipient: ChainId,
1606        latest_height: BlockHeight,
1607    ) -> Result<bool, WorkerError> {
1608        // Reconcile the outbox indices with the *current* tracked set before draining the counter
1609        // and checking delivery.
1610        let tracked = self.reconcile_tracked_outboxes().await?;
1611        // The indices are now reconciled to the tracked set, so `all_messages_delivered_up_to`
1612        // (the `outbox_counters` fast path) is already the complete answer for tracked chains.
1613        Ok(self
1614            .chain
1615            .mark_messages_as_received(
1616                &recipient,
1617                latest_height,
1618                tracked.as_deref().map(|h| h.inner()),
1619            )
1620            .await?
1621            && self.chain.all_messages_delivered_up_to(latest_height))
1622    }
1623
1624    /// Notifies delivery waiters that all messages up to `height` have been delivered.
1625    pub(crate) fn notify_delivery(&self, height: BlockHeight) {
1626        self.delivery_notifier.notify(height);
1627    }
1628
1629    /// Processes a batch of cross-chain requests, performing at most one `save()`.
1630    ///
1631    /// Both update and confirmation requests are handled together so that a
1632    /// single write-lock acquisition covers all pending work for the chain.
1633    pub(crate) async fn process_batch(
1634        &mut self,
1635        requests: Vec<BatchRequest>,
1636    ) -> Result<(), WorkerError> {
1637        let mut update_results = Vec::new();
1638        let mut confirm_results = Vec::new();
1639        let mut need_save = false;
1640        let mut need_rollback = false;
1641        let mut recovery_error = None;
1642        let mut max_delivered_height: Option<BlockHeight> = None;
1643
1644        for request in requests {
1645            match request {
1646                BatchRequest::Update {
1647                    origin,
1648                    bundles,
1649                    previous_height,
1650                    result_sender,
1651                } => {
1652                    if need_rollback {
1653                        send_result(result_sender, Err(WorkerError::BatchRolledBack));
1654                        continue;
1655                    }
1656                    let result = self
1657                        .process_cross_chain_update(origin, bundles, previous_height)
1658                        .await;
1659                    let update_result = match result {
1660                        Ok(update_result) => update_result,
1661                        Err(error) => {
1662                            need_rollback = true;
1663                            let (recovery, to_send) = classify_processing_error(error);
1664                            recovery_error = recovery_error.or(recovery);
1665                            send_result(result_sender, Err(to_send));
1666                            continue;
1667                        }
1668                    };
1669                    match &update_result {
1670                        CrossChainUpdateResult::Updated(_) => need_save = true,
1671                        CrossChainUpdateResult::GapDetected { .. }
1672                        | CrossChainUpdateResult::NothingToDo => {}
1673                    }
1674                    update_results.push((result_sender, update_result));
1675                }
1676                BatchRequest::Confirm {
1677                    recipient,
1678                    latest_height,
1679                    result_sender,
1680                } => {
1681                    if need_rollback {
1682                        send_result(result_sender, Err(WorkerError::BatchRolledBack));
1683                        continue;
1684                    }
1685                    match self
1686                        .confirm_updated_recipient(recipient, latest_height)
1687                        .await
1688                    {
1689                        Ok(fully_delivered) => {
1690                            need_save = true;
1691                            if fully_delivered {
1692                                max_delivered_height = Some(
1693                                    max_delivered_height
1694                                        .map_or(latest_height, |h| h.max(latest_height)),
1695                                );
1696                            }
1697                            confirm_results.push((result_sender, recipient));
1698                        }
1699                        Err(error) => {
1700                            need_rollback = true;
1701                            let (recovery, to_send) = classify_processing_error(error);
1702                            recovery_error = recovery_error.or(recovery);
1703                            send_result(result_sender, Err(to_send));
1704                        }
1705                    }
1706                }
1707            }
1708        }
1709        let mut save_error = None;
1710        if !need_rollback && need_save {
1711            if let Err(error) = self.save().await {
1712                tracing::error!(%error, "failed to save batch; rolling back");
1713                need_rollback = true;
1714                save_error = Some(error);
1715            }
1716        }
1717        if need_rollback {
1718            for (result_sender, _) in update_results {
1719                send_result(result_sender, Err(WorkerError::BatchRolledBack));
1720            }
1721            for (result_sender, _) in confirm_results {
1722                send_result(result_sender, Err(WorkerError::BatchRolledBack));
1723            }
1724            // Surface an error that left the worker poisoned or the chain state
1725            // corrupted so `chain_write` can evict or reset it. A `must_reload_view`
1726            // error only reaches us from a failed `save`, since resolving the journal
1727            // (the operation that raises it) happens only in `write_batch`. A
1728            // processing step instead can raise `CorruptedChainState` while reconciling
1729            // outboxes or message counters (see `confirm_updated_recipient`), which
1730            // calls for a reset rather than eviction. Ordinary processing errors (bad
1731            // height, validation failures) leave the worker healthy after rollback, so
1732            // they return `Ok` and were already reported to their individual senders.
1733            return match save_error.or(recovery_error) {
1734                Some(error) => Err(error),
1735                None => Ok(()),
1736            };
1737        }
1738
1739        if let Some(height) = max_delivered_height {
1740            self.notify_delivery(height);
1741        }
1742
1743        for (result_sender, update_result) in update_results {
1744            send_result(result_sender, Ok(update_result));
1745        }
1746        for (result_sender, recipient) in confirm_results {
1747            let result = self
1748                .create_cross_chain_actions_for_recipient(recipient)
1749                .await;
1750            send_result(result_sender, result);
1751        }
1752        Ok(())
1753    }
1754
1755    /// Handles a `RevertConfirm` request: walks backward through
1756    /// `previous_message_blocks` to find all block heights that sent messages to
1757    /// `recipient` starting from the latest down to `retransmit_from`, re-adds them
1758    /// to the outbox, and creates cross-chain update actions to resend the bundles.
1759    #[instrument(skip_all, fields(
1760        chain_id = %self.chain_id(),
1761        %recipient,
1762        %retransmit_from,
1763    ))]
1764    pub(crate) async fn handle_revert_confirm(
1765        &mut self,
1766        recipient: ChainId,
1767        retransmit_from: BlockHeight,
1768    ) -> Result<NetworkActions, WorkerError> {
1769        self.reconcile_tracked_outboxes().await?;
1770        // 1. Walk backward through previous_message_blocks to collect all heights
1771        //    that sent messages to this recipient, from the latest down to retransmit_from.
1772        let Some(latest_height) = self
1773            .chain
1774            .execution_state
1775            .previous_message_blocks
1776            .get(&recipient)
1777            .await?
1778        else {
1779            warn!("RevertConfirm: no record of sending to {recipient}");
1780            return Ok(NetworkActions::default());
1781        };
1782
1783        let mut heights_to_re_add = Vec::new();
1784        let mut current_height = latest_height;
1785        while current_height >= retransmit_from {
1786            // We arrived at current_height via previous_message_blocks links, starting from the
1787            // chain state and following the links downwards. So these blocks should all be in
1788            // `block_hashes` already.
1789            heights_to_re_add.push(current_height);
1790            // Load the block at current_height to find the previous message block
1791            let hash = match &*self
1792                .chain
1793                .block_hashes_for_heights([current_height])
1794                .await?
1795            {
1796                [hash] => *hash,
1797                _ => {
1798                    return Err(WorkerError::BlockHashNotFound {
1799                        height: current_height,
1800                        chain_id: self.chain_id(),
1801                    })
1802                }
1803            };
1804            let block = self
1805                .read_confirmed_blocks(&[hash])
1806                .await?
1807                .pop()
1808                .flatten()
1809                .ok_or_else(|| WorkerError::LocalBlockNotFound {
1810                    height: current_height,
1811                    chain_id: self.chain_id(),
1812                })?;
1813            match block.block().body.previous_message_blocks.get(&recipient) {
1814                Some((_, prev_height)) if *prev_height >= retransmit_from => {
1815                    current_height = *prev_height;
1816                }
1817                _ => break,
1818            }
1819        }
1820
1821        // 2. Re-add the heights to the outbox.
1822        let new_heights = self
1823            .chain
1824            .outboxes
1825            .try_load_entry_mut(&recipient)
1826            .await?
1827            .revert(&heights_to_re_add)
1828            .await?;
1829
1830        if new_heights.is_empty() {
1831            debug!("RevertConfirm: all heights already in outbox for {recipient}");
1832            return Ok(NetworkActions::default());
1833        }
1834
1835        // 3. Update the indices only for tracked recipients (mirroring `process_outgoing_messages`):
1836        //    an untracked recipient keeps its re-added outbox queue but is not counted or indexed.
1837        let new_heights_len = new_heights.len();
1838        if self.is_tracked(&recipient) {
1839            for h in new_heights {
1840                *self.chain.outbox_counters.get_mut().entry(h).or_default() += 1;
1841            }
1842            self.chain.nonempty_outboxes.get_mut().insert(recipient);
1843        }
1844
1845        // 4. Create cross-chain requests for this recipient.
1846        let actions = self
1847            .create_cross_chain_actions_for_recipient(recipient)
1848            .await?;
1849
1850        // 5. Save chain state.
1851        self.save().await?;
1852
1853        warn!(
1854            "RevertConfirm: re-added {new_heights_len} heights to outbox for {recipient}, \
1855            starting from height {retransmit_from}"
1856        );
1857
1858        Ok(actions)
1859    }
1860
1861    /// If the config enables corruption recovery and the min-duration guard is
1862    /// satisfied, resets the chain state and re-executes all confirmed blocks.
1863    /// Returns `RevertConfirm` requests to dispatch, or `None` if no reset happened.
1864    pub(crate) async fn maybe_reset_corrupted_chain_state(
1865        &mut self,
1866    ) -> Result<Option<Vec<CrossChainRequest>>, WorkerError> {
1867        let Some(min_duration) = self.config.reset_on_corrupted_chain_state else {
1868            return Ok(None);
1869        };
1870        let chain_id = self.chain_id();
1871        if !self.config.recovery_allowed_for(&chain_id) {
1872            return Ok(None);
1873        }
1874        let local_time = self.storage.clock().current_time();
1875        let initialized_time = *self.chain.chain_initialized_at.get();
1876        let elapsed = local_time.duration_since(initialized_time);
1877        if elapsed < min_duration {
1878            warn!(
1879                %chain_id, ?elapsed, ?min_duration,
1880                "Not resetting corrupted chain state; not enough time elapsed \
1881                since the chain was last initialized"
1882            );
1883            return Ok(None);
1884        }
1885        warn!(%chain_id, "Corrupted chain state detected; resetting and re-executing");
1886        Ok(Some(self.reset_and_reexecute_chain().await?))
1887    }
1888
1889    /// Resets the chain state completely and re-executes all confirmed blocks from storage.
1890    /// Returns a `RevertConfirm` request for every known sender so they resend cross-chain
1891    /// messages that may have been lost during the reset.
1892    #[instrument(skip_all, fields(
1893        chain_id = %self.chain_id(),
1894    ))]
1895    pub(crate) async fn reset_and_reexecute_chain(
1896        &mut self,
1897    ) -> Result<Vec<CrossChainRequest>, WorkerError> {
1898        let chain_id = self.chain_id();
1899        let tip_height = self.chain.tip_state.get().next_block_height;
1900
1901        // 1. Collect all sender chain IDs and block hashes before clearing.
1902        let sender_ids = self.chain.inboxes.indices().await?;
1903        let block_hashes = self.chain.block_hashes.index_values().await?;
1904        // Re-execution starts from the latest checkpoint, if any: replaying the checkpoint
1905        // block onto the wiped (height-0) chain installs its snapshot, so the blocks below it
1906        // never need to be replayed. Without a checkpoint, this is `0` and the whole chain is
1907        // replayed.
1908        let restore_from =
1909            (*self.chain.latest_checkpoint_height.get()).unwrap_or(BlockHeight::ZERO);
1910
1911        // 2. Snapshot safety-critical manager state so that we cannot be tricked
1912        //    into double-signing if the reset wipes votes we already cast.
1913        let manager_snapshot = ManagerSafetySnapshot::capture(&self.chain.manager).await?;
1914
1915        // 3. Wipe every key under this chain's storage root and reload a fresh
1916        //    `ChainStateView`. `ChainStateView::clear` + `save` would only reset
1917        //    the in-memory view; `HistoricallyHashableView` deliberately keeps
1918        //    its `stored_hash` across clears, so the historical hash would carry
1919        //    over from the pre-reset state and the replayed block outcomes would
1920        //    never match the original certificates' `state_hash`. Deleting the
1921        //    storage prefix and reloading is equivalent to creating the chain
1922        //    from scratch, which is what replay expects.
1923        self.wipe_and_reload_chain().await?;
1924        self.knows_chain_is_active = false;
1925        warn!(
1926            %chain_id,
1927            "Cleared chain state up to height {tip_height}; \
1928            re-executing blocks from height {restore_from}"
1929        );
1930
1931        // 4. Re-load and re-process the certificates from the latest checkpoint onward. The
1932        //    first one lands on the wiped, height-0 chain with a tip gap: if it is a checkpoint
1933        //    `process_confirmed_block` installs its snapshot before executing, and otherwise
1934        //    (height 0) it executes directly. The remaining blocks are contiguous.
1935        let total = block_hashes
1936            .iter()
1937            .filter(|(height, _)| *height >= restore_from)
1938            .count();
1939        let mut replayed = 0;
1940        for (height, hash) in block_hashes {
1941            if height < restore_from {
1942                continue;
1943            }
1944            if replayed % 1000 == 0 {
1945                info!(
1946                    %chain_id, replayed, total,
1947                    "Re-executing confirmed blocks after reset"
1948                );
1949            }
1950            replayed += 1;
1951            let cert = self
1952                .storage
1953                .read_certificate(hash)
1954                .await?
1955                .map(CacheArc::unwrap_or_clone)
1956                .ok_or_else(|| WorkerError::LocalBlockNotFound { height, chain_id })?;
1957            Box::pin(self.process_confirmed_block(cert, ProcessConfirmedBlockMode::Execute, None))
1958                .await?;
1959        }
1960
1961        // 5. Restore any previously cast votes and locking block so we cannot be
1962        //    asked to sign a conflicting statement at the same height/round. Votes
1963        //    in the manager always belong to the pending height (one past the tip),
1964        //    so restoring is only meaningful if re-execution landed at the same
1965        //    tip. Otherwise the restored state would refer to a stale pending
1966        //    height and could only break the manager's invariants without any
1967        //    safety benefit — so we drop the snapshot in that case.
1968        let new_tip_height = self.chain.tip_state.get().next_block_height;
1969        if new_tip_height == tip_height {
1970            manager_snapshot.restore(&mut self.chain.manager)?;
1971            self.save().await?;
1972        } else {
1973            warn!(
1974                %tip_height, %new_tip_height,
1975                "Dropping manager snapshot: pre-reset tip differs from post-reset tip"
1976            );
1977        }
1978
1979        // 6. Build RevertConfirm requests so each sender resends messages we may
1980        //    have lost during the reset.
1981        let revert_requests = sender_ids
1982            .into_iter()
1983            .map(|sender| CrossChainRequest::RevertConfirm {
1984                sender,
1985                recipient: chain_id,
1986                retransmit_from: BlockHeight::ZERO,
1987            })
1988            .collect::<Vec<_>>();
1989
1990        warn!(
1991            tip_height = %self.chain.tip_state.get().next_block_height,
1992            num_revert_confirms = revert_requests.len(),
1993            "Chain reset and re-executed; sending RevertConfirm to senders"
1994        );
1995
1996        Ok(revert_requests)
1997    }
1998
1999    #[instrument(skip_all, fields(
2000        chain_id = %self.chain_id(),
2001        num_trackers = %new_trackers.len()
2002    ))]
2003    pub(crate) async fn update_received_certificate_trackers(
2004        &mut self,
2005        new_trackers: BTreeMap<ValidatorPublicKey, u64>,
2006    ) -> Result<(), WorkerError> {
2007        self.chain
2008            .update_received_certificate_trackers(new_trackers);
2009        self.save().await?;
2010        Ok(())
2011    }
2012
2013    /// Returns the preprocessed block hashes in the given height range.
2014    #[instrument(skip_all, fields(
2015        chain_id = %self.chain_id(),
2016        start = %start,
2017        end = %end
2018    ))]
2019    pub(crate) async fn get_preprocessed_block_hashes(
2020        &self,
2021        start: BlockHeight,
2022        end: BlockHeight,
2023    ) -> Result<Vec<CryptoHash>, WorkerError> {
2024        let mut hashes = Vec::new();
2025        let mut height = start;
2026        while height < end {
2027            match self.chain.block_hashes.get(&height).await? {
2028                Some(hash) => hashes.push(hash),
2029                None => break,
2030            }
2031            height = height.try_add_one()?;
2032        }
2033        Ok(hashes)
2034    }
2035
2036    /// Returns the next block height to receive from an inbox.
2037    #[instrument(skip_all, fields(
2038        chain_id = %self.chain_id(),
2039        origin = %origin
2040    ))]
2041    pub(crate) async fn get_inbox_next_height(
2042        &self,
2043        origin: ChainId,
2044    ) -> Result<BlockHeight, WorkerError> {
2045        Ok(match self.chain.inboxes.try_load_entry(&origin).await? {
2046            Some(inbox) => inbox.next_block_height_to_receive()?,
2047            None => BlockHeight::ZERO,
2048        })
2049    }
2050
2051    /// Returns the locking blobs for the given blob IDs.
2052    /// Returns `Ok(None)` if any of the blobs is not found.
2053    #[instrument(skip_all, fields(
2054        chain_id = %self.chain_id(),
2055        num_blob_ids = %blob_ids.len()
2056    ))]
2057    pub(crate) async fn get_locking_blobs(
2058        &self,
2059        blob_ids: Vec<BlobId>,
2060    ) -> Result<Option<Vec<Blob>>, WorkerError> {
2061        let results = self
2062            .chain
2063            .manager
2064            .locking_blobs
2065            .multi_get(&blob_ids)
2066            .await?;
2067        Ok(results.into_iter().collect())
2068    }
2069
2070    /// Gets block hashes for specified heights.
2071    pub(crate) async fn get_block_hashes(
2072        &self,
2073        heights: Vec<BlockHeight>,
2074    ) -> Result<Vec<CryptoHash>, WorkerError> {
2075        Ok(self.chain.block_hashes_for_heights(heights).await?)
2076    }
2077
2078    /// Gets proposed blobs from the manager for specified blob IDs.
2079    pub(crate) async fn get_proposed_blobs(
2080        &self,
2081        blob_ids: Vec<BlobId>,
2082    ) -> Result<Vec<Blob>, WorkerError> {
2083        let results = self
2084            .chain
2085            .manager
2086            .proposed_blobs
2087            .multi_get(&blob_ids)
2088            .await?;
2089        let mut blobs = Vec::with_capacity(blob_ids.len());
2090        let mut missing = Vec::new();
2091        for (blob_id, maybe_blob) in blob_ids.into_iter().zip(results) {
2092            match maybe_blob {
2093                Some(blob) => blobs.push(blob),
2094                None => missing.push(blob_id),
2095            }
2096        }
2097        if !missing.is_empty() {
2098            return Err(WorkerError::BlobsNotFound(missing));
2099        }
2100        Ok(blobs)
2101    }
2102
2103    /// Gets event subscriptions.
2104    pub(crate) async fn get_event_subscriptions(
2105        &self,
2106    ) -> Result<EventSubscriptionsResult, WorkerError> {
2107        Ok(self
2108            .chain
2109            .execution_state
2110            .system
2111            .event_subscriptions
2112            .index_values()
2113            .await?)
2114    }
2115
2116    /// Gets a stream's [`StreamCounts`]: the next expected event index and the lowest readable
2117    /// index (the first event published since the most recent checkpoint). Both default to 0 for
2118    /// a stream with no events yet. They come from the same `next_expected_events` entry, so they
2119    /// are mutually consistent and both reflect every block this node has processed — including
2120    /// ones it only preprocessed.
2121    pub(crate) async fn get_stream_indices(
2122        &self,
2123        stream_id: StreamId,
2124    ) -> Result<StreamCounts, WorkerError> {
2125        Ok(self
2126            .chain
2127            .next_expected_events
2128            .get(&stream_id)
2129            .await?
2130            .unwrap_or_default())
2131    }
2132
2133    /// Gets the `next_expected_events` indices for the given streams.
2134    pub(crate) async fn get_next_expected_events(
2135        &self,
2136        stream_ids: Vec<StreamId>,
2137    ) -> Result<BTreeMap<StreamId, u32>, WorkerError> {
2138        let values = self
2139            .chain
2140            .next_expected_events
2141            .multi_get(&stream_ids)
2142            .await?;
2143        Ok(stream_ids
2144            .into_iter()
2145            .zip(values)
2146            .filter_map(|(id, val)| Some((id, val?.next_index)))
2147            .collect())
2148    }
2149
2150    /// Gets received certificate trackers.
2151    pub(crate) async fn get_received_certificate_trackers(
2152        &self,
2153    ) -> Result<HashMap<ValidatorPublicKey, u64>, WorkerError> {
2154        Ok(self.chain.received_certificate_trackers.get().clone())
2155    }
2156
2157    /// Gets tip state and outbox info for next_outbox_heights calculation.
2158    pub(crate) async fn get_tip_state_and_outbox_info(
2159        &self,
2160        receiver_id: ChainId,
2161    ) -> Result<(BlockHeight, Option<BlockHeight>), WorkerError> {
2162        let next_block_height = self.chain.tip_state.get().next_block_height;
2163        let next_height_to_schedule = self
2164            .chain
2165            .outboxes
2166            .try_load_entry(&receiver_id)
2167            .await?
2168            .map(|outbox| *outbox.next_height_to_schedule.get());
2169        Ok((next_block_height, next_height_to_schedule))
2170    }
2171
2172    /// Gets the next height to preprocess.
2173    pub(crate) fn get_next_height_to_preprocess(&self) -> BlockHeight {
2174        *self.chain.next_height_to_preprocess.get()
2175    }
2176
2177    /// Attempts to vote for a leader timeout, if possible.
2178    #[instrument(skip_all, fields(
2179        chain_id = %self.chain_id(),
2180        height = %height,
2181        round = %round
2182    ))]
2183    async fn vote_for_leader_timeout(
2184        &mut self,
2185        height: BlockHeight,
2186        round: Round,
2187    ) -> Result<(), WorkerError> {
2188        let chain = &mut self.chain;
2189        ensure!(
2190            height == chain.tip_state.get().next_block_height,
2191            WorkerError::UnexpectedBlockHeight {
2192                expected_block_height: chain.tip_state.get().next_block_height,
2193                found_block_height: height
2194            }
2195        );
2196        let epoch = chain.execution_state.system.epoch.get();
2197        let chain_id = chain.chain_id();
2198        let key_pair = self.config.key_pair();
2199        let local_time = self.storage.clock().current_time();
2200        if chain
2201            .manager
2202            .create_timeout_vote(chain_id, height, round, *epoch, key_pair, local_time)?
2203        {
2204            self.save().await?;
2205        }
2206        Ok(())
2207    }
2208
2209    /// Votes for falling back to a public chain.
2210    ///
2211    /// Fallback is triggered when the chain is in epoch `e` and epoch `e+1` has been created
2212    /// on the admin chain longer than the configured `fallback_duration` ago.
2213    #[instrument(skip_all, fields(
2214        chain_id = %self.chain_id()
2215    ))]
2216    async fn vote_for_fallback(&mut self) -> Result<(), WorkerError> {
2217        let chain = &mut self.chain;
2218        let epoch = *chain.execution_state.system.epoch.get();
2219        let Some(admin_chain_id) = chain.execution_state.system.admin_chain_id.get() else {
2220            return Ok(());
2221        };
2222
2223        // Check if epoch e+1 exists on the admin chain and when it was created.
2224        let next_epoch_index = epoch.0.saturating_add(1);
2225        let event_id = EventId {
2226            chain_id: *admin_chain_id,
2227            stream_id: StreamId::system(EPOCH_STREAM_NAME),
2228            index: next_epoch_index,
2229        };
2230
2231        let Some(event_bytes) = self.storage.read_event(event_id).await? else {
2232            return Ok(()); // Next epoch doesn't exist yet.
2233        };
2234
2235        let event_data: EpochEventData = bcs::from_bytes(&event_bytes)?;
2236        let elapsed = self
2237            .storage
2238            .clock()
2239            .current_time()
2240            .delta_since(event_data.timestamp);
2241        if elapsed >= chain.ownership().await?.timeout_config.fallback_duration {
2242            let chain_id = chain.chain_id();
2243            let height = chain.tip_state.get().next_block_height;
2244            let key_pair = self.config.key_pair();
2245            if chain
2246                .manager
2247                .vote_fallback(chain_id, height, epoch, key_pair)
2248            {
2249                self.save().await?;
2250            }
2251        }
2252        Ok(())
2253    }
2254
2255    #[instrument(skip_all, fields(
2256        chain_id = %self.chain_id(),
2257        blob_id = %blob.id()
2258    ))]
2259    pub(crate) async fn handle_pending_blob(
2260        &mut self,
2261        blob: Blob,
2262    ) -> Result<ChainInfoResponse, WorkerError> {
2263        let mut was_expected = self
2264            .chain
2265            .pending_validated_blobs
2266            .maybe_insert(&blob)
2267            .await?;
2268        for (_, mut pending_blobs) in self
2269            .chain
2270            .pending_proposed_blobs
2271            .try_load_all_entries_mut()
2272            .await?
2273        {
2274            if !pending_blobs.validated.get() {
2275                let (_, committee) = self.chain.current_committee().await?;
2276                let policy = committee.policy();
2277                policy
2278                    .check_blob_size(blob.content())
2279                    .with_execution_context(ChainExecutionContext::Block)?;
2280                ensure!(
2281                    u64::try_from(pending_blobs.pending_blobs.iterative_count().await?)
2282                        .is_ok_and(|count| count < policy.maximum_published_blobs),
2283                    WorkerError::TooManyPublishedBlobs(policy.maximum_published_blobs)
2284                );
2285            }
2286            was_expected = was_expected || pending_blobs.maybe_insert(&blob).await?;
2287        }
2288        ensure!(was_expected, WorkerError::UnexpectedBlob);
2289        self.save().await?;
2290        self.chain_info_response().await
2291    }
2292
2293    /// Returns a stored [`Certificate`] for the chain's block at the requested [`BlockHeight`].
2294    ///
2295    /// Does not need `&mut self` because the chain is eagerly initialized when the
2296    /// chain handle is created.
2297    #[cfg(with_testing)]
2298    #[instrument(skip_all, fields(
2299        chain_id = %self.chain_id(),
2300        height = %height
2301    ))]
2302    pub(crate) async fn read_certificate(
2303        &self,
2304        height: BlockHeight,
2305    ) -> Result<Option<CacheArc<ConfirmedBlockCertificate>>, WorkerError> {
2306        let certificate_hash = match self.chain.block_hashes.get(&height).await? {
2307            Some(hash) => hash,
2308            None => return Ok(None),
2309        };
2310        let certificate = self
2311            .storage
2312            .read_certificate(certificate_hash)
2313            .await?
2314            .ok_or(WorkerError::BlocksNotFound(vec![certificate_hash]))?;
2315        Ok(Some(certificate))
2316    }
2317
2318    /// Queries an application's state on the chain.
2319    #[instrument(skip_all, fields(
2320        chain_id = %self.chain_id(),
2321        query_application_id = %query.application_id()
2322    ))]
2323    pub(crate) async fn query_application(
2324        &mut self,
2325        query: Query,
2326        block_hash: Option<CryptoHash>,
2327    ) -> Result<(QueryOutcome, BlockHeight), WorkerError> {
2328        self.initialize_and_save_if_needed().await?;
2329        let next_block_height = self.chain.tip_state.get().next_block_height;
2330        let local_time = self.storage.clock().current_time();
2331        // Try to use a cached execution state for the requested block.
2332        // We want to pretend that this block is committed, so we set the next block height.
2333        let cached_state = block_hash
2334            .zip(self.execution_state_cache.as_ref())
2335            .and_then(|(h, cache)| Some(h).zip(cache.remove(&h)));
2336        if let Some((requested_block, mut state)) = cached_state {
2337            let next_block_height = next_block_height
2338                .try_add_one()
2339                .expect("block height to not overflow");
2340            let context = QueryContext {
2341                chain_id: self.chain_id(),
2342                next_block_height,
2343                local_time,
2344            };
2345            let outcome = state
2346                .with_context(|ctx| {
2347                    self.chain
2348                        .execution_state
2349                        .context()
2350                        .clone_with_base_key(ctx.base_key().bytes.clone())
2351                })
2352                .await
2353                .query_application(context, query, self.service_runtime_endpoint.as_mut())
2354                .await
2355                .with_execution_context(ChainExecutionContext::Query)?;
2356            if let Some(cache) = &self.execution_state_cache {
2357                cache.insert(&requested_block, state);
2358            }
2359            Ok((outcome, next_block_height))
2360        } else {
2361            if block_hash.is_some() {
2362                tracing::debug!(
2363                    "requested block hash not found in cache, querying committed state"
2364                );
2365            }
2366            let outcome = self
2367                .chain
2368                .query_application(local_time, query, self.service_runtime_endpoint.as_mut())
2369                .await?;
2370            Ok((outcome, next_block_height))
2371        }
2372    }
2373
2374    /// Returns an application's description by reading the blob directly from storage.
2375    ///
2376    /// Does not track blob usage (which requires `&mut self`), making it safe for
2377    /// concurrent reads. Blob tracking is only relevant during block execution and is
2378    /// always rolled back for read-only queries.
2379    #[instrument(skip_all, fields(
2380        chain_id = %self.chain_id(),
2381        application_id = %application_id
2382    ))]
2383    pub(crate) async fn describe_application_readonly(
2384        &self,
2385        application_id: ApplicationId,
2386    ) -> Result<ApplicationDescription, WorkerError> {
2387        let blob_id = application_id.description_blob_id();
2388        let blob = self
2389            .storage
2390            .read_blob(blob_id)
2391            .await?
2392            .ok_or(WorkerError::BlobsNotFound(vec![blob_id]))?;
2393        Ok(bcs::from_bytes(blob.bytes())?)
2394    }
2395
2396    /// Executes a block without persisting any changes to the state, with a specified
2397    /// policy for handling bundle failures.
2398    ///
2399    /// The block may be modified to reflect the actual executed transactions
2400    /// (bundles may be rejected or removed based on the policy).
2401    #[instrument(skip_all, fields(
2402        chain_id = %self.chain_id(),
2403        block_height = %block.height
2404    ))]
2405    pub(crate) async fn stage_block_execution(
2406        &mut self,
2407        block: ProposedBlock,
2408        round: Option<u32>,
2409        published_blobs: &[Blob],
2410        policy: BundleExecutionPolicy,
2411    ) -> Result<
2412        (
2413            ProposedBlock,
2414            Block,
2415            ChainInfoResponse,
2416            ResourceTracker,
2417            HashSet<ChainId>,
2418        ),
2419        WorkerError,
2420    > {
2421        self.initialize_and_save_if_needed().await?;
2422        let local_time = self.storage.clock().current_time();
2423        let (_, committee) = self.chain.current_committee().await?;
2424        block.check_proposal_size(committee.policy().maximum_block_proposal_size)?;
2425
2426        self.chain
2427            .remove_bundles_from_inboxes(block.timestamp, true, block.incoming_bundles())
2428            .await?;
2429        let (executed_block, resource_tracker, never_reject_origins) =
2430            Box::pin(self.execute_block(
2431                block,
2432                local_time,
2433                round,
2434                published_blobs,
2435                BlockExecution::StageProposal { policy },
2436            ))
2437            .await?;
2438
2439        // No need to sign: only used internally.
2440        let info = ChainInfo::from_chain_view(&mut self.chain).await?;
2441        let mut response = ChainInfoResponse::new(info, None);
2442        if let Some(owner) = executed_block.header.authenticated_owner {
2443            response.info.requested_owner_balance = self
2444                .chain
2445                .execution_state
2446                .system
2447                .balances
2448                .get(&owner)
2449                .await?;
2450        }
2451
2452        let (proposed_block, _) = executed_block.clone().into_proposal();
2453        Ok((
2454            proposed_block,
2455            executed_block,
2456            response,
2457            resource_tracker,
2458            never_reject_origins,
2459        ))
2460    }
2461
2462    /// Validates and executes a block proposed to extend this chain.
2463    ///
2464    /// Returns network actions alongside the result so the caller can dispatch them
2465    /// even when the proposal is rejected: a `HasIncompatibleConfirmedVote` rejection
2466    /// can still advance `current_round` via `update_signed_proposal`, and subscribers
2467    /// need the resulting `NewRound` notification.
2468    #[instrument(skip_all, fields(
2469        chain_id = %self.chain_id(),
2470        block_height = %proposal.content.block.height
2471    ))]
2472    pub(crate) async fn handle_block_proposal(
2473        &mut self,
2474        proposal: BlockProposal,
2475    ) -> (Result<ChainInfoResponse, WorkerError>, NetworkActions) {
2476        #[cfg(with_metrics)]
2477        metrics::BLOCK_PROPOSALS_RECEIVED_TOTAL.inc();
2478        let chain_id = proposal.content.block.chain_id;
2479        let height = proposal.content.block.height;
2480        let old_round = self.chain.manager.current_round();
2481        match self.try_handle_block_proposal(proposal).await {
2482            Ok((response, actions)) => (Ok(response), actions),
2483            Err(err) => {
2484                let error_type = err.error_type();
2485                #[cfg(with_metrics)]
2486                metrics::BLOCK_PROPOSALS_REJECTED_TOTAL
2487                    .with_label_values(&[error_type.as_str()])
2488                    .inc();
2489                debug!(%chain_id, %height, %error_type, "Block proposal rejected");
2490                // Even on error, the manager's `current_round` may have advanced
2491                // (the `HasIncompatibleConfirmedVote` recovery path calls
2492                // `update_signed_proposal`). Surface the resulting `NewRound`
2493                // notification so subscribers can react.
2494                let actions = if self.chain.manager.current_round() != old_round {
2495                    self.create_network_actions(Some(old_round))
2496                        .await
2497                        .unwrap_or_default()
2498                } else {
2499                    NetworkActions::default()
2500                };
2501                (Err(err), actions)
2502            }
2503        }
2504    }
2505
2506    async fn try_handle_block_proposal(
2507        &mut self,
2508        proposal: BlockProposal,
2509    ) -> Result<(ChainInfoResponse, NetworkActions), WorkerError> {
2510        self.initialize_and_save_if_needed().await?;
2511        proposal
2512            .check_invariants()
2513            .map_err(|msg| WorkerError::InvalidBlockProposal(msg.to_string()))?;
2514        proposal.check_signature()?;
2515        let owner = proposal.owner();
2516        let BlockProposal {
2517            content,
2518            original_proposal,
2519            signature: _,
2520        } = &proposal;
2521        let block = &content.block;
2522        let chain = &self.chain;
2523        // Check if the chain is ready for this new block proposal.
2524        chain.tip_state.get().verify_block_chaining(block)?;
2525        // Check the epoch.
2526        let (epoch, committee) = chain.current_committee().await?;
2527        check_block_epoch(epoch, block.chain_id, block.epoch)?;
2528        let policy = committee.policy().clone();
2529        block.check_proposal_size(policy.maximum_block_proposal_size)?;
2530        // Check the authentication of the block.
2531        ensure!(
2532            chain.manager.can_propose(&owner, proposal.content.round),
2533            WorkerError::InvalidOwner
2534        );
2535        let old_round = self.chain.manager.current_round();
2536        match original_proposal {
2537            None => {
2538                if let Some(signer) = block.authenticated_owner {
2539                    // Check the authentication of the operations in the new block.
2540                    ensure!(signer == owner, WorkerError::InvalidSigner(owner));
2541                }
2542            }
2543            Some(OriginalProposal::Regular { certificate }) => {
2544                // Verify that this block has been validated by a quorum before.
2545                certificate.check(&committee)?;
2546            }
2547            Some(OriginalProposal::Fast(signature)) => {
2548                let original_proposal = BlockProposal {
2549                    content: ProposalContent {
2550                        block: content.block.clone(),
2551                        round: Round::Fast,
2552                        outcome: None,
2553                    },
2554                    signature: *signature,
2555                    original_proposal: None,
2556                };
2557                let super_owner = original_proposal.owner();
2558                ensure!(
2559                    chain
2560                        .manager
2561                        .ownership
2562                        .get()
2563                        .super_owners
2564                        .contains(&super_owner),
2565                    WorkerError::InvalidOwner
2566                );
2567                if let Some(signer) = block.authenticated_owner {
2568                    // Check the authentication of the operations in the new block.
2569                    ensure!(signer == super_owner, WorkerError::InvalidSigner(signer));
2570                }
2571                original_proposal.check_signature()?;
2572            }
2573        }
2574        let local_time = self.storage.clock().current_time();
2575        match chain.manager.check_proposed_block(&proposal) {
2576            Ok(manager::Outcome::Skip) => {
2577                // We already voted for this block.
2578                return Ok((self.chain_info_response().await?, NetworkActions::default()));
2579            }
2580            Ok(manager::Outcome::Accept) => {}
2581            Err(err) => {
2582                // A `HasIncompatibleConfirmedVote` rejection means the proposer is at a
2583                // round we'd otherwise be happy to sign at; only our prior confirmed vote
2584                // prevents us from voting. Record the proposal so `current_round` still
2585                // tracks the round the proposer is in — without it, the chain can wedge.
2586                // Other rejections (e.g. `WrongRound`, `InsufficientRound`) mean the
2587                // proposal is not actually valid for the chain's current state, so we
2588                // shouldn't let it advance our round.
2589                if matches!(err, ChainError::HasIncompatibleConfirmedVote(_, _))
2590                    && self
2591                        .chain
2592                        .manager
2593                        .update_signed_proposal(&proposal, local_time)
2594                {
2595                    self.save().await?;
2596                }
2597                return Err(err.into());
2598            }
2599        }
2600
2601        // Make sure we remember that a proposal was signed, to determine the correct round to
2602        // propose in.
2603        if self
2604            .chain
2605            .manager
2606            .update_signed_proposal(&proposal, local_time)
2607        {
2608            self.save().await?;
2609        }
2610
2611        let published_blobs = self.load_proposal_blobs(&proposal).await?;
2612        let ProposalContent {
2613            block,
2614            round,
2615            outcome,
2616        } = content;
2617
2618        if self.config.key_pair().is_some()
2619            && block.timestamp.duration_since(local_time) > self.config.block_time_grace_period
2620        {
2621            return Err(WorkerError::InvalidTimestamp {
2622                local_time,
2623                block_timestamp: block.timestamp,
2624                block_time_grace_period: self.config.block_time_grace_period,
2625            });
2626        }
2627        // Note: WorkerState::handle_block_proposal delays processing proposals with future
2628        // timestamps (within the grace period) before acquiring the chain lock. By the time
2629        // we reach here, the block timestamp should be in the past or very close to current time.
2630
2631        self.chain
2632            .remove_bundles_from_inboxes(block.timestamp, true, block.incoming_bundles())
2633            .await?;
2634        let block = if let Some(outcome) = outcome {
2635            outcome.clone().with(proposal.content.block.clone())
2636        } else {
2637            let (executed_block, _resource_tracker, _) = Box::pin(self.execute_block(
2638                block.clone(),
2639                local_time,
2640                round.multi_leader(),
2641                &published_blobs,
2642                BlockExecution::HandleProposal,
2643            ))
2644            .await?;
2645            executed_block
2646        };
2647
2648        ensure!(
2649            !round.is_fast() || !block.has_oracle_responses(),
2650            WorkerError::FastBlockUsingOracles
2651        );
2652        let chain = &mut self.chain;
2653        // Don't save the changes since the block is not confirmed yet.
2654        chain.rollback();
2655
2656        // Create the vote and store it in the chain state.
2657        let blobs = self
2658            .get_required_blobs(proposal.expected_blob_ids(), block.created_blobs())
2659            .await?;
2660        let key_pair = self.config.key_pair();
2661        let manager = &mut self.chain.manager;
2662        match manager.create_vote(&proposal, block, key_pair, local_time, blobs)? {
2663            // Cache the value we voted on, so the client doesn't have to send it again.
2664            Some(Either::Left(vote)) => {
2665                self.block_values
2666                    .insert_hashed(Cow::Borrowed(vote.value.inner()));
2667            }
2668            Some(Either::Right(vote)) => {
2669                self.block_values
2670                    .insert_hashed(Cow::Borrowed(vote.value.inner()));
2671            }
2672            None => (),
2673        }
2674        self.save().await?;
2675        let actions = self.create_network_actions(Some(old_round)).await?;
2676        Ok((self.chain_info_response().await?, actions))
2677    }
2678
2679    /// Prepares a [`ChainInfoResponse`] for a [`ChainInfoQuery`].
2680    #[instrument(skip_all, fields(
2681        chain_id = %self.chain_id()
2682    ))]
2683    async fn prepare_chain_info_response(
2684        &mut self,
2685        query: ChainInfoQuery,
2686    ) -> Result<ChainInfoResponse, WorkerError> {
2687        self.initialize_and_save_if_needed().await?;
2688        let mut info = ChainInfo::from_chain_view(&mut self.chain).await?;
2689        let chain = &self.chain;
2690        if query.request_owner_balance == AccountOwner::CHAIN {
2691            info.requested_owner_balance = Some(*chain.execution_state.system.balance.get());
2692        } else {
2693            info.requested_owner_balance = chain
2694                .execution_state
2695                .system
2696                .balances
2697                .get(&query.request_owner_balance)
2698                .await?;
2699        }
2700        if let Some(next_block_height) = query.test_next_block_height {
2701            // If not, send the same error as if a block with next_block_height was proposed.
2702            ensure!(
2703                chain.tip_state.get().next_block_height == next_block_height,
2704                WorkerError::UnexpectedBlockHeight {
2705                    expected_block_height: chain.tip_state.get().next_block_height,
2706                    found_block_height: next_block_height,
2707                }
2708            );
2709        }
2710        if query.request_pending_message_bundles {
2711            let mut bundles = Vec::new();
2712            let nonempty_origins: Vec<ChainId> =
2713                chain.nonempty_inboxes.get().iter().copied().collect();
2714            #[cfg(with_metrics)]
2715            metrics::NUM_INBOXES
2716                .with_label_values(&[])
2717                .observe(nonempty_origins.len() as f64);
2718            let is_closed = *chain.execution_state.system.closed.get();
2719            let action = if is_closed {
2720                MessageAction::Reject
2721            } else {
2722                MessageAction::Accept
2723            };
2724            let inboxes = chain.inboxes.try_load_entries(&nonempty_origins).await?;
2725            for (origin, inbox) in nonempty_origins.into_iter().zip(inboxes) {
2726                let inbox = inbox.ok_or_else(|| {
2727                    ChainError::InternalError(format!("Missing inbox for origin {origin}"))
2728                })?;
2729                for bundle in inbox.added_bundles.elements().await? {
2730                    bundles.push(IncomingBundle {
2731                        origin,
2732                        bundle,
2733                        action,
2734                    });
2735                }
2736            }
2737            if is_closed && !bundles.is_empty() {
2738                info!(
2739                    chain_id = %chain.chain_id(),
2740                    count = bundles.len(),
2741                    "Auto-rejecting all incoming message bundles because the chain is closed"
2742                );
2743            }
2744            info.requested_pending_message_bundles = bundles;
2745        }
2746        let hashes = chain
2747            .block_hashes_for_heights(query.request_sent_certificate_hashes_by_heights)
2748            .await?;
2749        info.requested_sent_certificate_hashes = hashes;
2750        if let Some(start) = query.request_received_log_excluding_first_n {
2751            let start = usize::try_from(start).map_err(|_| ArithmeticError::Overflow)?;
2752            let max_received_log_entries = self.config.chain_info_max_received_log_entries;
2753            let end = start
2754                .saturating_add(max_received_log_entries)
2755                .min(chain.received_log.count());
2756            info.requested_received_log = chain.received_log.read(start..end).await?;
2757            #[cfg(with_metrics)]
2758            metrics::RECEIVED_LOG_QUERY_ENTRIES.observe(info.requested_received_log.len() as f64);
2759        }
2760        if query.request_manager_values {
2761            info.manager.add_values(&chain.manager);
2762        }
2763        if !query.request_previous_event_blocks.is_empty() {
2764            let stream_ids = query.request_previous_event_blocks;
2765            let heights = chain
2766                .execution_state
2767                .previous_event_blocks
2768                .multi_get(&stream_ids)
2769                .await?;
2770            let mut streams_with_heights = Vec::new();
2771            for (stream_id, height) in stream_ids.into_iter().zip(heights) {
2772                if let Some(height) = height {
2773                    streams_with_heights.push((stream_id, height));
2774                }
2775            }
2776            let hashes = chain
2777                .block_hashes
2778                .multi_get(streams_with_heights.iter().map(|(_, height)| height))
2779                .await?;
2780            for (maybe_hash, (stream_id, height)) in hashes.into_iter().zip(streams_with_heights) {
2781                let hash = maybe_hash.ok_or_else(|| WorkerError::BlockHashNotFound {
2782                    height,
2783                    chain_id: info.chain_id,
2784                })?;
2785                info.requested_previous_event_blocks
2786                    .insert(stream_id, (height, hash));
2787            }
2788        }
2789        if query.request_latest_checkpoint_height {
2790            info.requested_latest_checkpoint_height = *self.chain.latest_checkpoint_height.get();
2791        }
2792        Ok(ChainInfoResponse::new(info, self.config.key_pair()))
2793    }
2794
2795    /// Executes a block with a specified policy for handling bundle failures.
2796    ///
2797    /// The block may be modified to reflect the actual executed transactions.
2798    #[instrument(skip_all, fields(
2799        chain_id = %self.chain_id(),
2800        block_height = %block.height
2801    ))]
2802    async fn execute_block(
2803        &mut self,
2804        block: ProposedBlock,
2805        local_time: Timestamp,
2806        round: Option<u32>,
2807        published_blobs: &[Blob],
2808        execution: BlockExecution,
2809    ) -> Result<(Block, ResourceTracker, HashSet<ChainId>), WorkerError> {
2810        let (proposed_block, outcome, resource_tracker, never_reject_origins) = Box::pin(
2811            self.chain
2812                .execute_block(block, local_time, round, published_blobs, execution),
2813        )
2814        .await?;
2815        let executed_block = Block::new(proposed_block, outcome);
2816        let block_hash = executed_block.hash();
2817        if let Some(cache) = &self.execution_state_cache {
2818            cache.insert(
2819                &block_hash,
2820                Box::pin(
2821                    self.chain
2822                        .execution_state
2823                        .with_context(|ctx| InactiveContext(ctx.base_key().clone())),
2824                )
2825                .await,
2826            );
2827        }
2828        Ok((executed_block, resource_tracker, never_reject_origins))
2829    }
2830
2831    /// Initializes and saves the current chain if it is not active yet.
2832    #[instrument(skip_all, fields(
2833        chain_id = %self.chain_id()
2834    ))]
2835    pub(crate) async fn initialize_and_save_if_needed(&mut self) -> Result<(), WorkerError> {
2836        if !self.knows_chain_is_active {
2837            let local_time = self.storage.clock().current_time();
2838            self.chain.initialize_if_needed(local_time).await?;
2839            self.save().await?;
2840            self.knows_chain_is_active = true;
2841        }
2842        Ok(())
2843    }
2844
2845    pub(crate) async fn chain_info_response(&mut self) -> Result<ChainInfoResponse, WorkerError> {
2846        let info = ChainInfo::from_chain_view(&mut self.chain).await?;
2847        Ok(ChainInfoResponse::new(info, self.config.key_pair()))
2848    }
2849
2850    /// Stores the chain state in persistent storage.
2851    ///
2852    /// If the save fails, the worker is marked as poisoned and must be reloaded.
2853    #[instrument(skip_all, fields(
2854        chain_id = %self.chain_id()
2855    ))]
2856    pub(crate) async fn save(&mut self) -> Result<(), WorkerError> {
2857        if let Err(error) = self.chain.save().await {
2858            if error.must_reload_view() {
2859                tracing::error!(
2860                    ?error,
2861                    chain_id = %self.chain_id(),
2862                    "Chain save failed with a nonrecoverable error; marking worker as poisoned"
2863                );
2864                self.poisoned = true;
2865            }
2866            return Err(WorkerError::ViewError(error));
2867        }
2868        Ok(())
2869    }
2870
2871    /// Deletes every key under this chain's storage root and replaces `self.chain`
2872    /// with a freshly loaded (empty) view. If either the write or the reload fails,
2873    /// the worker is marked as poisoned: the partial deletion may have left
2874    /// storage inconsistent with the in-memory view, so it cannot be reused.
2875    #[instrument(skip_all, fields(
2876        chain_id = %self.chain_id()
2877    ))]
2878    async fn wipe_and_reload_chain(&mut self) -> Result<(), WorkerError> {
2879        let context = self.chain.context().clone();
2880        let mut batch = Batch::new();
2881        batch.delete_key_prefix(Vec::new());
2882        if let Err(error) = context.store().write_batch(batch).await {
2883            tracing::error!(
2884                ?error,
2885                chain_id = %self.chain_id(),
2886                "Wiping chain storage failed; marking worker as poisoned"
2887            );
2888            self.poisoned = true;
2889            return Err(WorkerError::PoisonedWorker);
2890        }
2891        match ChainStateView::load(context).await {
2892            Ok(chain) => {
2893                self.chain = chain;
2894                Ok(())
2895            }
2896            Err(error) => {
2897                tracing::error!(
2898                    ?error,
2899                    chain_id = %self.chain_id(),
2900                    "Reloading chain after wipe failed; marking worker as poisoned"
2901                );
2902                self.poisoned = true;
2903                Err(WorkerError::PoisonedWorker)
2904            }
2905        }
2906    }
2907}
2908
2909/// Classifies an error returned while processing a single cross-chain batch request.
2910///
2911/// If the error indicates a poisoned worker or corrupted chain state, it is returned
2912/// as the first element so `process_batch` can hand it to `chain_write` for recovery
2913/// (eviction or reset), and the originating caller is told `BatchRolledBack` so it
2914/// retries against a freshly loaded worker. Any other error leaves the worker healthy
2915/// after rollback and is reported to the caller unchanged.
2916fn classify_processing_error(error: WorkerError) -> (Option<WorkerError>, WorkerError) {
2917    if error.must_reload_view() || error.indicates_corrupted_chain_state() {
2918        (Some(error), WorkerError::BatchRolledBack)
2919    } else {
2920        (None, error)
2921    }
2922}
2923
2924/// Sends a result through a oneshot channel, logging at `debug` level if the
2925/// receiver has been dropped.
2926pub(crate) fn send_result<T>(sender: oneshot::Sender<T>, value: T) {
2927    if sender.send(value).is_err() {
2928        tracing::debug!("cannot send cross-chain result; receiver dropped");
2929    }
2930}
2931
2932/// Returns the missing indices and corresponding blob_ids.
2933fn missing_indices_blob_ids(maybe_blobs: &[(BlobId, Option<Blob>)]) -> (Vec<usize>, Vec<BlobId>) {
2934    let mut missing_indices = Vec::new();
2935    let mut missing_blob_ids = Vec::new();
2936    for (index, (blob_id, blob)) in maybe_blobs.iter().enumerate() {
2937        if blob.is_none() {
2938            missing_indices.push(index);
2939            missing_blob_ids.push(*blob_id);
2940        }
2941    }
2942    (missing_indices, missing_blob_ids)
2943}
2944
2945/// Returns the blob IDs whose corresponding value is `None`.
2946fn missing_blob_ids<'a>(
2947    maybe_blobs: impl IntoIterator<Item = (&'a BlobId, &'a Option<Blob>)>,
2948) -> Vec<BlobId> {
2949    maybe_blobs
2950        .into_iter()
2951        .filter(|(_, maybe_blob)| maybe_blob.is_none())
2952        .map(|(blob_id, _)| *blob_id)
2953        .collect()
2954}
2955
2956/// Returns an error if the block is not at the expected epoch.
2957fn check_block_epoch(
2958    chain_epoch: Epoch,
2959    block_chain: ChainId,
2960    block_epoch: Epoch,
2961) -> Result<(), WorkerError> {
2962    ensure!(
2963        block_epoch == chain_epoch,
2964        WorkerError::InvalidEpoch {
2965            chain_id: block_chain,
2966            epoch: block_epoch,
2967            chain_epoch
2968        }
2969    );
2970    Ok(())
2971}