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