Skip to main content

linera_chain/
chain.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
6    sync::Arc,
7};
8
9use allocative::Allocative;
10use linera_base::{
11    crypto::{CryptoHash, ValidatorPublicKey},
12    data_types::{
13        ApplicationDescription, ApplicationPermissions, ArithmeticError, Blob, BlockHeight, Epoch,
14        NonCanonicalBTreeMap, NonCanonicalBTreeSet, OracleResponse, Timestamp,
15    },
16    ensure,
17    hashed::Hashed,
18    identifiers::{AccountOwner, ApplicationId, BlobType, ChainId, StreamId},
19    ownership::ChainOwnership,
20    time::{Duration, Instant},
21};
22use linera_execution::{
23    committee::Committee, system::EPOCH_STREAM_NAME, ExecutionRuntimeContext, ExecutionStateView,
24    Message, Operation, OutgoingMessage, Query, QueryContext, QueryOutcome, ResourceController,
25    ResourceTracker, ServiceRuntimeEndpoint, TransactionTracker,
26    FLAG_MANDATORY_APPS_NEED_ACCEPTED_MESSAGE,
27};
28use linera_views::{
29    bucket_queue_view::BucketQueueView,
30    context::Context,
31    log_view::LogView,
32    map_view::MapView,
33    reentrant_collection_view::{ReadGuardedView, ReentrantCollectionView},
34    register_view::RegisterView,
35    set_view::SetView,
36    views::{ClonableView, RootView, View},
37};
38use serde::{Deserialize, Serialize};
39use tracing::{info, instrument, warn};
40
41use crate::{
42    block::{Block, ConfirmedBlock},
43    block_tracker::BlockExecutionTracker,
44    data_types::{
45        BlockExecutionOutcome, BundleExecutionPolicy, BundleFailurePolicy, ChainAndHeight,
46        IncomingBundle, MessageAction, MessageBundle, ProposedBlock, Transaction,
47    },
48    inbox::{Cursor, InboxError, InboxStateView},
49    manager::ChainManager,
50    outbox::OutboxStateView,
51    pending_blobs::PendingBlobsView,
52    ChainError, ChainExecutionContext, ExecutionError, ExecutionResultExt,
53};
54
55#[cfg(test)]
56#[path = "unit_tests/chain_tests.rs"]
57mod chain_tests;
58
59#[cfg(with_metrics)]
60use linera_base::prometheus_util::MeasureLatency;
61
62/// The protocol phase a block is executed in. Recorded as the `phase` label on the
63/// block-execution metrics so the three distinct paths — staging a proposal, validating a
64/// received proposal, and committing a confirmed certificate — are separate time series in
65/// Prometheus.
66///
67/// Every path that executes a block must name its phase explicitly: there is no `Default`,
68/// so a new caller cannot compile without choosing one, and no execution can land in an
69/// unlabeled or silently-mislabeled bucket.
70///
71/// The label strings are derived from the variant names in `snake_case`; they are part of the
72/// metrics wire format, so `metrics_label_values_are_stable` pins them against an accidental
73/// variant rename.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr)]
75#[strum(serialize_all = "snake_case")]
76pub enum BlockExecutionPhase {
77    /// A block proposer staging (building) its own block (`stage_block_execution`).
78    StageProposal,
79    /// A validator validating a received block proposal (`handle_block_proposal`).
80    HandleProposal,
81    /// A validator executing a confirmed certificate before committing it
82    /// (`process_confirmed_block`).
83    HandleConfirmed,
84}
85
86/// What a call to [`ChainStateView::execute_block`] is doing, carrying exactly the inputs that
87/// are legal for that phase.
88///
89/// Bundling the phase together with the replayed oracle responses and the bundle-execution
90/// policy makes the illegal combinations unrepresentable: only [`StageProposal`] may choose a
91/// policy (and thus `AutoRetry`), and only [`HandleConfirmed`] carries oracle responses to
92/// replay — so "replay oracle responses while auto-retrying" cannot be constructed.
93///
94/// [`StageProposal`]: BlockExecution::StageProposal
95/// [`HandleConfirmed`]: BlockExecution::HandleConfirmed
96pub enum BlockExecution {
97    /// A proposer staging (building) its own block. The bundle-failure policy is caller-chosen
98    /// (and may be `AutoRetry`); oracle responses are computed fresh, never replayed.
99    StageProposal {
100        /// How to handle failing bundles while building the proposal.
101        policy: BundleExecutionPolicy,
102    },
103    /// A validator validating a received block proposal. Bundles must abort on failure (the
104    /// proposal is fixed) and oracle responses are computed fresh.
105    HandleProposal,
106    /// A validator executing a confirmed certificate before committing it. Bundles must abort
107    /// on failure and the certificate's recorded oracle responses are replayed for determinism.
108    HandleConfirmed {
109        /// The oracle responses recorded in the certificate, replayed to reproduce the outcome.
110        oracle_responses: Vec<Vec<OracleResponse>>,
111    },
112}
113
114impl BlockExecution {
115    /// The protocol phase this execution represents, used to label execution metrics and spans.
116    pub fn phase(&self) -> BlockExecutionPhase {
117        match self {
118            BlockExecution::StageProposal { .. } => BlockExecutionPhase::StageProposal,
119            BlockExecution::HandleProposal => BlockExecutionPhase::HandleProposal,
120            BlockExecution::HandleConfirmed { .. } => BlockExecutionPhase::HandleConfirmed,
121        }
122    }
123
124    /// Splits into the oracle responses to replay (if any) and the bundle-execution policy.
125    fn into_oracle_and_policy(self) -> (Option<Vec<Vec<OracleResponse>>>, BundleExecutionPolicy) {
126        match self {
127            BlockExecution::StageProposal { policy } => (None, policy),
128            BlockExecution::HandleProposal => (None, BundleExecutionPolicy::committed()),
129            BlockExecution::HandleConfirmed { oracle_responses } => {
130                (Some(oracle_responses), BundleExecutionPolicy::committed())
131            }
132        }
133    }
134}
135
136#[cfg(with_metrics)]
137pub(crate) mod metrics {
138    use std::sync::LazyLock;
139
140    use linera_base::prometheus_util::{
141        exponential_bucket_interval, register_histogram_vec, register_int_counter_vec,
142    };
143    use linera_execution::ResourceTracker;
144    use prometheus::{HistogramVec, IntCounterVec};
145
146    pub static NUM_BLOCKS_EXECUTED: LazyLock<IntCounterVec> = LazyLock::new(|| {
147        register_int_counter_vec(
148            "num_blocks_executed",
149            "Number of blocks executed",
150            &["phase"],
151        )
152    });
153
154    pub static BLOCK_EXECUTION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
155        register_histogram_vec(
156            "block_execution_latency",
157            "Block execution latency",
158            &["phase"],
159            exponential_bucket_interval(50.0_f64, 10_000_000.0),
160        )
161    });
162
163    #[cfg(with_metrics)]
164    pub static MESSAGE_EXECUTION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
165        register_histogram_vec(
166            "message_execution_latency",
167            "Message execution latency",
168            &["phase"],
169            exponential_bucket_interval(0.1_f64, 1_000_000.0),
170        )
171    });
172
173    pub static OPERATION_EXECUTION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
174        register_histogram_vec(
175            "operation_execution_latency",
176            "Operation execution latency",
177            &["phase"],
178            exponential_bucket_interval(0.1_f64, 1_000_000.0),
179        )
180    });
181
182    pub static WASM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
183        register_histogram_vec(
184            "wasm_fuel_used_per_block",
185            "Wasm fuel used per block",
186            &["phase"],
187            exponential_bucket_interval(10.0, 100_000_000.0),
188        )
189    });
190
191    pub static EVM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
192        register_histogram_vec(
193            "evm_fuel_used_per_block",
194            "EVM fuel used per block",
195            &["phase"],
196            exponential_bucket_interval(10.0, 100_000_000.0),
197        )
198    });
199
200    pub static VM_NUM_READS_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
201        register_histogram_vec(
202            "vm_num_reads_per_block",
203            "VM number of reads per block",
204            &["phase"],
205            exponential_bucket_interval(0.1, 100.0),
206        )
207    });
208
209    pub static VM_BYTES_READ_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
210        register_histogram_vec(
211            "vm_bytes_read_per_block",
212            "VM number of bytes read per block",
213            &["phase"],
214            exponential_bucket_interval(0.1, 10_000_000.0),
215        )
216    });
217
218    pub static VM_BYTES_WRITTEN_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
219        register_histogram_vec(
220            "vm_bytes_written_per_block",
221            "VM number of bytes written per block",
222            &["phase"],
223            exponential_bucket_interval(0.1, 10_000_000.0),
224        )
225    });
226
227    pub static STATE_HASH_COMPUTATION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
228        register_histogram_vec(
229            "state_hash_computation_latency",
230            "Time to recompute the state hash, in microseconds",
231            &["phase"],
232            exponential_bucket_interval(1.0, 2_000_000.0),
233        )
234    });
235
236    pub static NUM_OUTBOXES: LazyLock<HistogramVec> = LazyLock::new(|| {
237        register_histogram_vec(
238            "num_outboxes",
239            "Number of outboxes",
240            &[],
241            exponential_bucket_interval(1.0, 1_000_000.0),
242        )
243    });
244
245    pub static OUTBOX_COUNTERS_SIZE: LazyLock<HistogramVec> = LazyLock::new(|| {
246        register_histogram_vec(
247            "outbox_counters_size",
248            "Number of entries in the outbox_counters map (in-flight message heights)",
249            &[],
250            exponential_bucket_interval(1.0, 1_000_000.0),
251        )
252    });
253
254    /// Tracks block execution metrics in Prometheus, labeled by the execution `phase`.
255    pub(crate) fn track_block_metrics(
256        tracker: &ResourceTracker,
257        phase: super::BlockExecutionPhase,
258    ) {
259        let phase: &[&str] = &[phase.into()];
260        NUM_BLOCKS_EXECUTED.with_label_values(phase).inc();
261        WASM_FUEL_USED_PER_BLOCK
262            .with_label_values(phase)
263            .observe(tracker.wasm_fuel as f64);
264        EVM_FUEL_USED_PER_BLOCK
265            .with_label_values(phase)
266            .observe(tracker.evm_fuel as f64);
267        VM_NUM_READS_PER_BLOCK
268            .with_label_values(phase)
269            .observe(tracker.read_operations as f64);
270        VM_BYTES_READ_PER_BLOCK
271            .with_label_values(phase)
272            .observe(tracker.bytes_read as f64);
273        VM_BYTES_WRITTEN_PER_BLOCK
274            .with_label_values(phase)
275            .observe(tracker.bytes_written as f64);
276    }
277}
278
279/// The BCS-serialized size of an empty [`Block`].
280pub(crate) const EMPTY_BLOCK_SIZE: usize = 94;
281
282/// An origin, cursor and timestamp of a unskippable bundle in our inbox.
283#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
284#[derive(Debug, Clone, Serialize, Deserialize, Allocative)]
285pub struct TimestampedBundleInInbox {
286    /// The origin and cursor of the bundle.
287    pub entry: BundleInInbox,
288    /// The timestamp when the bundle was added to the inbox.
289    pub seen: Timestamp,
290}
291
292/// An origin and cursor of a unskippable bundle that is no longer in our inbox.
293#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
294#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Allocative)]
295pub struct BundleInInbox {
296    /// The origin from which we received the bundle.
297    pub origin: ChainId,
298    /// The cursor of the bundle in the inbox.
299    pub cursor: Cursor,
300}
301
302// The `TimestampedBundleInInbox` is a relatively small type, so a total
303// of 100 seems reasonable for the storing of the data.
304const TIMESTAMPBUNDLE_BUCKET_SIZE: usize = 100;
305
306/// A set of fully-tracked chains. Wrapped in [`Hashed`] (as `Hashed<ChainIdSet>`) so the hash that
307/// identifies the set — stored in [`ChainStateView::outbox_index_tracked_hash`] to detect when the
308/// outbox indices must be reconciled — is computed once when the tracked set changes rather than on
309/// every cross-chain operation. The hash is order-independent because `BTreeSet` iterates in sorted
310/// order.
311#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
312pub struct ChainIdSet(pub BTreeSet<ChainId>);
313
314impl linera_base::crypto::BcsHashable<'_> for ChainIdSet {}
315
316impl std::ops::Deref for ChainIdSet {
317    type Target = BTreeSet<ChainId>;
318
319    fn deref(&self) -> &Self::Target {
320        &self.0
321    }
322}
323
324/// A view accessing the state of a chain.
325#[cfg_attr(
326    with_graphql,
327    derive(async_graphql::SimpleObject),
328    graphql(cache_control(no_cache))
329)]
330#[derive(Debug, RootView, ClonableView, Allocative)]
331#[allocative(bound = "C")]
332pub struct ChainStateView<C>
333where
334    C: Clone + Context + 'static,
335{
336    /// Execution state, including system and user applications.
337    pub execution_state: ExecutionStateView<C>,
338    /// Hash of the execution state.
339    pub execution_state_hash: RegisterView<C, Option<CryptoHash>>,
340
341    /// Block-chaining state.
342    pub tip_state: RegisterView<C, ChainTipState>,
343
344    /// Consensus state.
345    pub manager: ChainManager<C>,
346    /// Pending validated block that is still missing blobs.
347    /// The incomplete set of blobs for the pending validated block.
348    pub pending_validated_blobs: PendingBlobsView<C>,
349    /// The incomplete sets of blobs for upcoming proposals.
350    pub pending_proposed_blobs: ReentrantCollectionView<C, AccountOwner, PendingBlobsView<C>>,
351
352    /// Hashes of all certified blocks for this sender.
353    /// This ends with `block_hash` and has length `usize::from(next_block_height)`.
354    pub confirmed_log: LogView<C, CryptoHash>,
355    /// Sender chain and height of all certified blocks known as a receiver (local ordering).
356    pub received_log: LogView<C, ChainAndHeight>,
357    /// The number of `received_log` entries we have synchronized, for each validator.
358    pub received_certificate_trackers: RegisterView<C, HashMap<ValidatorPublicKey, u64>>,
359
360    /// Mailboxes used to receive messages indexed by their origin.
361    pub inboxes: ReentrantCollectionView<C, ChainId, InboxStateView<C>>,
362    /// A queue of unskippable bundles, with the timestamp when we added them to the inbox.
363    pub unskippable_bundles:
364        BucketQueueView<C, TimestampedBundleInInbox, TIMESTAMPBUNDLE_BUCKET_SIZE>,
365    /// Unskippable bundles that have been removed but are still in the queue.
366    pub removed_unskippable_bundles: SetView<C, BundleInInbox>,
367    /// The heights of previous blocks that sent messages to the same recipients.
368    pub previous_message_blocks: MapView<C, ChainId, BlockHeight>,
369    /// The heights of previous blocks that published events to the same streams.
370    pub previous_event_blocks: MapView<C, StreamId, BlockHeight>,
371    /// Mailboxes used to send messages, indexed by their target.
372    pub outboxes: ReentrantCollectionView<C, ChainId, OutboxStateView<C>>,
373    /// Number of outgoing messages in flight for each block height.
374    /// We use a `RegisterView` to prioritize speed for small maps.
375    pub outbox_counters: RegisterView<C, NonCanonicalBTreeMap<BlockHeight, u32>>,
376    /// Outboxes with at least one pending message. This allows us to avoid loading all outboxes.
377    pub nonempty_outboxes: RegisterView<C, NonCanonicalBTreeSet<ChainId>>,
378
379    /// Blocks that have been verified but not executed yet, and that may not be contiguous.
380    pub preprocessed_blocks: MapView<C, BlockHeight, CryptoHash>,
381
382    /// The indices of next events we expect to see per stream (could be ahead of the last
383    /// executed block in sparse chains).
384    pub next_expected_events: MapView<C, StreamId, u32>,
385
386    /// Inboxes with at least one pending added bundle. This allows us to avoid loading all
387    /// inboxes. `None` means the set hasn't been computed yet for this chain (backwards
388    /// compatibility with pre-existing database entries).
389    pub nonempty_inboxes: RegisterView<C, Option<NonCanonicalBTreeSet<ChainId>>>,
390
391    /// The local wall-clock time when block 0 was last executed. Used to prevent
392    /// reset-on-incorrect-outcome from looping: if not enough time has elapsed since
393    /// the last reset, the error is returned instead.
394    pub block_zero_executed_at: RegisterView<C, Timestamp>,
395
396    /// The hash of the set of fully-tracked chains that `nonempty_outboxes` and
397    /// `outbox_counters` were last reconciled against. On a client these two indices only hold
398    /// entries for tracked targets; when the tracked set changes this hash stops matching and the
399    /// indices are reconciled (`reconcile_outbox_index`). `None` means
400    /// they have never been filtered — a pre-existing database entry (migration), or a validator
401    /// that tracks all chains and never filters.
402    pub outbox_index_tracked_hash: RegisterView<C, Option<CryptoHash>>,
403}
404
405/// Block-chaining state.
406#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
407#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
408pub struct ChainTipState {
409    /// Hash of the latest certified block in this chain, if any.
410    pub block_hash: Option<CryptoHash>,
411    /// Sequence number tracking blocks.
412    pub next_block_height: BlockHeight,
413    /// Number of incoming message bundles.
414    pub num_incoming_bundles: u32,
415    /// Number of operations.
416    pub num_operations: u32,
417    /// Number of outgoing messages.
418    pub num_outgoing_messages: u32,
419}
420
421impl ChainTipState {
422    /// Checks that the proposed block is suitable, i.e. at the expected height and with the
423    /// expected parent.
424    pub fn verify_block_chaining(&self, new_block: &ProposedBlock) -> Result<(), ChainError> {
425        ensure!(
426            new_block.height == self.next_block_height,
427            ChainError::UnexpectedBlockHeight {
428                expected_block_height: self.next_block_height,
429                found_block_height: new_block.height
430            }
431        );
432        ensure!(
433            new_block.previous_block_hash == self.block_hash,
434            ChainError::UnexpectedPreviousBlockHash
435        );
436        Ok(())
437    }
438
439    /// Returns `true` if the validated block's height is below the tip height. Returns an error if
440    /// it is higher than the tip.
441    pub fn already_validated_block(&self, height: BlockHeight) -> Result<bool, ChainError> {
442        ensure!(
443            self.next_block_height >= height,
444            ChainError::MissingEarlierBlocks {
445                current_block_height: self.next_block_height,
446            }
447        );
448        Ok(self.next_block_height > height)
449    }
450
451    /// Checks if the measurement counters would be valid.
452    pub fn update_counters(
453        &mut self,
454        transactions: &[Transaction],
455        messages: &[Vec<OutgoingMessage>],
456    ) -> Result<(), ChainError> {
457        let mut num_incoming_bundles = 0u32;
458        let mut num_operations = 0u32;
459
460        for transaction in transactions {
461            match transaction {
462                Transaction::ReceiveMessages(_) => {
463                    num_incoming_bundles = num_incoming_bundles
464                        .checked_add(1)
465                        .ok_or(ArithmeticError::Overflow)?;
466                }
467                Transaction::ExecuteOperation(_) => {
468                    num_operations = num_operations
469                        .checked_add(1)
470                        .ok_or(ArithmeticError::Overflow)?;
471                }
472            }
473        }
474
475        self.num_incoming_bundles = self
476            .num_incoming_bundles
477            .checked_add(num_incoming_bundles)
478            .ok_or(ArithmeticError::Overflow)?;
479
480        self.num_operations = self
481            .num_operations
482            .checked_add(num_operations)
483            .ok_or(ArithmeticError::Overflow)?;
484
485        let num_outgoing_messages = u32::try_from(messages.iter().map(Vec::len).sum::<usize>())
486            .map_err(|_| ArithmeticError::Overflow)?;
487        self.num_outgoing_messages = self
488            .num_outgoing_messages
489            .checked_add(num_outgoing_messages)
490            .ok_or(ArithmeticError::Overflow)?;
491
492        Ok(())
493    }
494}
495
496impl<C> ChainStateView<C>
497where
498    C: Context + Clone + 'static,
499    C::Extra: ExecutionRuntimeContext,
500{
501    /// Returns the [`ChainId`] of the chain this [`ChainStateView`] represents.
502    pub fn chain_id(&self) -> ChainId {
503        self.context().extra().chain_id()
504    }
505
506    #[instrument(skip_all, fields(
507        chain_id = %self.chain_id(),
508    ))]
509    /// Executes the given query against an application on this chain.
510    pub async fn query_application(
511        &mut self,
512        local_time: Timestamp,
513        query: Query,
514        service_runtime_endpoint: Option<&mut ServiceRuntimeEndpoint>,
515    ) -> Result<QueryOutcome, ChainError> {
516        let context = QueryContext {
517            chain_id: self.chain_id(),
518            next_block_height: self.tip_state.get().next_block_height,
519            local_time,
520        };
521        self.execution_state
522            .query_application(context, query, service_runtime_endpoint)
523            .await
524            .with_execution_context(ChainExecutionContext::Query)
525    }
526
527    #[instrument(skip_all, fields(
528        chain_id = %self.chain_id(),
529        application_id = %application_id
530    ))]
531    /// Returns the description of the application with the given ID.
532    pub async fn describe_application(
533        &mut self,
534        application_id: ApplicationId,
535    ) -> Result<ApplicationDescription, ChainError> {
536        self.execution_state
537            .system
538            .describe_application(application_id, &mut TransactionTracker::default())
539            .await
540            .with_execution_context(ChainExecutionContext::DescribeApplication)
541    }
542
543    #[instrument(skip_all, fields(
544        chain_id = %self.chain_id(),
545        target = %target,
546        height = %height
547    ))]
548    /// Marks all messages sent to `target` up to the given height as received, returning whether
549    /// the outbox changed.
550    pub async fn mark_messages_as_received(
551        &mut self,
552        target: &ChainId,
553        height: BlockHeight,
554        tracked: Option<&ChainIdSet>,
555    ) -> Result<bool, ChainError> {
556        let mut outbox = self.outboxes.try_load_entry_mut(target).await?;
557        let updates = outbox.mark_messages_as_received(height).await?;
558        if updates.is_empty() {
559            return Ok(false);
560        }
561        // `outbox_counters` is keyed by block height and shared across all recipients of that
562        // block, but only counts targets we index: every chain on a validator (`tracked == None`),
563        // or tracked targets on a client. An untracked target was never counted, so confirming it
564        // must NOT touch the counters at all — a present `counter[height]` belongs to a tracked
565        // sibling recipient of the same block and must be left intact. We only drain the queue
566        // (done above) for such a target.
567        if tracked.is_none_or(|tracked| tracked.contains(target)) {
568            for update in updates {
569                let counter = self
570                    .outbox_counters
571                    .get_mut()
572                    .get_mut(&update)
573                    .ok_or_else(|| {
574                        ChainError::CorruptedChainState("message counter should be present".into())
575                    })?;
576                *counter = counter.checked_sub(1).ok_or(ArithmeticError::Underflow)?;
577                if *counter == 0 {
578                    // Important for the test in `all_messages_delivered_up_to`.
579                    self.outbox_counters.get_mut().remove(&update);
580                }
581            }
582        }
583        if outbox.queue.count() == 0 {
584            self.nonempty_outboxes.get_mut().remove(target);
585            // If the outbox is empty and not ahead of the executed blocks, remove it.
586            if *outbox.next_height_to_schedule.get() <= self.tip_state.get().next_block_height {
587                self.outboxes.remove_entry(target)?;
588            }
589        }
590        #[cfg(with_metrics)]
591        metrics::NUM_OUTBOXES
592            .with_label_values(&[])
593            .observe(self.nonempty_outboxes.get().len() as f64);
594        #[cfg(with_metrics)]
595        metrics::OUTBOX_COUNTERS_SIZE
596            .with_label_values(&[])
597            .observe(self.outbox_counters.get().len() as f64);
598        Ok(true)
599    }
600
601    /// Returns true if there are no more outgoing messages in flight up to the given
602    /// block height.
603    pub fn all_messages_delivered_up_to(&self, height: BlockHeight) -> bool {
604        tracing::debug!(
605            "Messages left in {:.8}'s outbox: {:?}",
606            self.chain_id(),
607            self.outbox_counters.get()
608        );
609        if let Some((key, _)) = self.outbox_counters.get().first_key_value() {
610            key > &height
611        } else {
612            true
613        }
614    }
615
616    /// Invariant for the states of active chains.
617    pub async fn is_active(&self) -> Result<bool, ChainError> {
618        Ok(self.execution_state.system.is_active().await?)
619    }
620
621    /// Initializes the chain if it is not active yet.
622    pub async fn initialize_if_needed(&mut self, local_time: Timestamp) -> Result<(), ChainError> {
623        // Initialize ourselves.
624        if self
625            .execution_state
626            .system
627            .initialize_chain(self.chain_id())
628            .await
629            .with_execution_context(ChainExecutionContext::Block)?
630        {
631            // The chain was already initialized.
632            return Ok(());
633        }
634        // Recompute the state hash.
635        let hash = self.execution_state.crypto_hash_mut().await?;
636        self.execution_state_hash.set(Some(hash));
637        self.reset_chain_manager(BlockHeight(0), local_time).await?;
638        Ok(())
639    }
640
641    /// Returns the height of the highest block we have, plus one. Includes preprocessed blocks.
642    ///
643    /// The "+ 1" is so that it can be used in the same places as `next_block_height`.
644    pub async fn next_height_to_preprocess(&self) -> Result<BlockHeight, ChainError> {
645        // `indices()` returns heights in serialization order (BCS little-endian for `u64`),
646        // which does not match numeric order, so we take the numeric max rather than the last.
647        if let Some(height) = self.preprocessed_blocks.indices().await?.into_iter().max() {
648            return Ok(height.saturating_add(BlockHeight(1)));
649        }
650        Ok(self.tip_state.get().next_block_height)
651    }
652
653    /// Attempts to process a new `bundle` of messages from the given `origin`. Returns an
654    /// internal error if the bundle doesn't appear to be new, based on the sender's
655    /// height. The value `local_time` is specific to each validator and only used for
656    /// round timeouts.
657    ///
658    /// Returns `true` if incoming `Subscribe` messages created new outbox entries.
659    #[instrument(skip_all, fields(
660        chain_id = %self.chain_id(),
661        origin = %origin,
662        bundle_height = %bundle.height
663    ))]
664    pub async fn receive_message_bundle_with_inbox(
665        &mut self,
666        inbox: &mut InboxStateView<C>,
667        origin: &ChainId,
668        bundle: MessageBundle,
669        local_time: Timestamp,
670        add_to_received_log: bool,
671    ) -> Result<(), ChainError> {
672        assert!(!bundle.messages.is_empty());
673        let chain_id = self.chain_id();
674        tracing::trace!(
675            "Processing new messages from {origin} at height {}",
676            bundle.height,
677        );
678        let chain_and_height = ChainAndHeight {
679            chain_id: *origin,
680            height: bundle.height,
681        };
682
683        match self.initialize_if_needed(local_time).await {
684            Ok(_) => (),
685            // if the only issue was that we couldn't initialize the chain because of a
686            // missing chain description blob, we might still want to update the inbox
687            Err(ChainError::ExecutionError(exec_err, _))
688                if matches!(*exec_err, ExecutionError::BlobsNotFound(ref blobs)
689                if blobs.iter().all(|blob_id| {
690                    blob_id.blob_type == BlobType::ChainDescription && blob_id.hash == chain_id.0
691                })) => {}
692            err => {
693                return err;
694            }
695        }
696
697        // Process the inbox bundle and update the inbox state.
698        let newly_added = inbox
699            .add_bundle(bundle)
700            .await
701            .map_err(|error| match error {
702                InboxError::ViewError(error) => ChainError::ViewError(error),
703                error => ChainError::CorruptedChainState(format!(
704                    "while processing messages in certified block: {error}"
705                )),
706            })?;
707        if newly_added {
708            if let Some(set) = self.nonempty_inboxes.get_mut() {
709                set.insert(*origin);
710            }
711        }
712
713        // Remember the certificate for future validator/client synchronizations.
714        if add_to_received_log {
715            self.received_log.push(chain_and_height);
716        }
717        Ok(())
718    }
719
720    /// Updates the `received_log` trackers.
721    pub fn update_received_certificate_trackers(
722        &mut self,
723        new_trackers: BTreeMap<ValidatorPublicKey, u64>,
724    ) {
725        for (name, tracker) in new_trackers {
726            self.received_certificate_trackers
727                .get_mut()
728                .entry(name)
729                .and_modify(|t| {
730                    // Because several synchronizations could happen in parallel, we need to make
731                    // sure to never go backward.
732                    if tracker > *t {
733                        *t = tracker;
734                    }
735                })
736                .or_insert(tracker);
737        }
738    }
739
740    /// Returns the current epoch and committee of this chain.
741    pub async fn current_committee(&self) -> Result<(Epoch, Arc<Committee>), ChainError> {
742        self.execution_state
743            .system
744            .current_committee()
745            .await?
746            .ok_or_else(|| ChainError::InactiveChain(self.chain_id()))
747    }
748
749    /// Returns the ownership configuration of this chain.
750    pub async fn ownership(&self) -> Result<&ChainOwnership, ChainError> {
751        Ok(self.execution_state.system.ownership.get().await?)
752    }
753
754    /// Removes the incoming message bundles in the block from the inboxes.
755    ///
756    /// If `must_be_present` is `true`, an error is returned if any of the bundles have not been
757    /// added to the inbox yet. So this should be `true` if the bundles are in a block _proposal_,
758    /// and `false` if the block is already confirmed.
759    #[instrument(skip_all, fields(
760        chain_id = %self.chain_id(),
761    ))]
762    pub async fn remove_bundles_from_inboxes(
763        &mut self,
764        timestamp: Timestamp,
765        must_be_present: bool,
766        incoming_bundles: impl IntoIterator<Item = &IncomingBundle>,
767    ) -> Result<(), ChainError> {
768        let chain_id = self.chain_id();
769        let mut bundles_by_origin: BTreeMap<_, Vec<&MessageBundle>> = Default::default();
770        for IncomingBundle { bundle, origin, .. } in incoming_bundles {
771            ensure!(
772                bundle.timestamp <= timestamp,
773                ChainError::IncorrectBundleTimestamp {
774                    chain_id,
775                    bundle_timestamp: bundle.timestamp,
776                    block_timestamp: timestamp,
777                }
778            );
779            let bundles = bundles_by_origin.entry(*origin).or_default();
780            bundles.push(bundle);
781        }
782        let origins = bundles_by_origin.keys().copied().collect::<Vec<_>>();
783        let inboxes = self.inboxes.try_load_entries_mut(&origins).await?;
784        // When the bundles must already be present (block proposals), collect *every* missing
785        // `(origin, height)` rather than bailing on the first, so the caller can be told the
786        // full set of cross-chain updates to fetch in a single round-trip.
787        let mut missing_bundles = Vec::new();
788        for ((origin, bundles), mut inbox) in bundles_by_origin.into_iter().zip(inboxes) {
789            tracing::trace!(
790                "Removing [{}] from inbox for {origin}",
791                bundles
792                    .iter()
793                    .map(|bundle| bundle.height.to_string())
794                    .collect::<Vec<_>>()
795                    .join(", ")
796            );
797            for bundle in bundles {
798                // Mark the message as processed in the inbox.
799                let was_present = inbox
800                    .remove_bundle(bundle)
801                    .await
802                    .map_err(|error| (chain_id, origin, error))?;
803                if must_be_present && !was_present {
804                    missing_bundles.push((origin, bundle.height));
805                }
806            }
807            inbox.observe_size_metric();
808            if inbox.added_bundles.count() == 0 {
809                if let Some(set) = self.nonempty_inboxes.get_mut() {
810                    set.remove(&origin);
811                }
812            }
813        }
814        ensure!(
815            missing_bundles.is_empty(),
816            ChainError::MissingCrossChainUpdates {
817                chain_id,
818                bundles: missing_bundles,
819            }
820        );
821        Ok(())
822    }
823
824    /// Returns the chain IDs of all recipients for which a message is waiting in the outbox.
825    pub fn nonempty_outbox_chain_ids(&self) -> Vec<ChainId> {
826        self.nonempty_outboxes.get().iter().copied().collect()
827    }
828
829    /// Returns the outboxes for the given targets, or an error if any of them are missing.
830    pub async fn load_outboxes(
831        &self,
832        targets: &[ChainId],
833    ) -> Result<Vec<ReadGuardedView<OutboxStateView<C>>>, ChainError> {
834        let vec_of_options = self.outboxes.try_load_entries(targets).await?;
835        let optional_vec = vec_of_options.into_iter().collect::<Option<Vec<_>>>();
836        optional_vec.ok_or_else(|| ChainError::CorruptedChainState("Missing outboxes".into()))
837    }
838
839    /// Reconciles the `nonempty_outboxes` and `outbox_counters` indices from the retained outbox
840    /// queues, rebuilding only when the tracked set changed since the last call (i.e. the stored
841    /// [`Self::outbox_index_tracked_hash`] no longer matches). The per-target outbox *queues* in
842    /// `outboxes` are always kept; only these indices are filtered. Returns whether a rebuild
843    /// actually happened, so a read-only caller can skip persisting when nothing changed.
844    pub async fn reconcile_outbox_index(
845        &mut self,
846        tracked: Option<&Hashed<ChainIdSet>>,
847    ) -> Result<bool, ChainError> {
848        let digest = tracked.map(|tracked| tracked.hash());
849        if *self.outbox_index_tracked_hash.get() == digest {
850            return Ok(false);
851        }
852        self.nonempty_outboxes.get_mut().clear();
853        self.outbox_counters.get_mut().clear();
854        // In full mode (`None`) there is no tracked subset to iterate, so re-index from the keys of
855        // every retained outbox queue.
856        let targets = match tracked {
857            Some(tracked) => tracked.inner().iter().copied().collect::<Vec<_>>(),
858            None => self.outboxes.indices().await?,
859        };
860        for target in &targets {
861            let heights = {
862                let Some(outbox) = self.outboxes.try_load_entry(target).await? else {
863                    continue;
864                };
865                outbox.queue.elements().await?
866            };
867            if heights.is_empty() {
868                continue;
869            }
870            for height in heights {
871                *self.outbox_counters.get_mut().entry(height).or_default() += 1;
872            }
873            self.nonempty_outboxes.get_mut().insert(*target);
874        }
875        self.outbox_index_tracked_hash.set(digest);
876        Ok(true)
877    }
878
879    /// Returns whether the outbox index is already reconciled to `tracked` (the stored hash
880    /// matches), so the read-only network-actions path can read it without a write-lock rebuild.
881    pub fn outbox_index_is_reconciled(&self, tracked: Option<&Hashed<ChainIdSet>>) -> bool {
882        *self.outbox_index_tracked_hash.get() == tracked.map(|tracked| tracked.hash())
883    }
884
885    /// Executes a block with a specified policy for handling bundle failures.
886    #[instrument(skip_all, fields(
887        chain_id = %block.chain_id,
888        block_height = %block.height
889    ))]
890    #[expect(clippy::too_many_arguments)]
891    async fn execute_block_inner(
892        chain: &mut ExecutionStateView<C>,
893        confirmed_log: &LogView<C, CryptoHash>,
894        previous_message_blocks_view: &MapView<C, ChainId, BlockHeight>,
895        previous_event_blocks_view: &MapView<C, StreamId, BlockHeight>,
896        block: &mut ProposedBlock,
897        local_time: Timestamp,
898        round: Option<u32>,
899        published_blobs: &[Blob],
900        replaying_oracle_responses: Option<Vec<Vec<OracleResponse>>>,
901        exec_policy: BundleExecutionPolicy,
902        phase: BlockExecutionPhase,
903    ) -> Result<(BlockExecutionOutcome, ResourceTracker, HashSet<ChainId>), ChainError> {
904        #[cfg(with_metrics)]
905        let block_execution_latency =
906            metrics::BLOCK_EXECUTION_LATENCY.with_label_values(&[phase.into()]);
907        #[cfg(with_metrics)]
908        let _execution_latency = block_execution_latency.measure_latency_us();
909        chain.system.timestamp.set(block.timestamp);
910
911        let committee_policy = chain
912            .system
913            .current_committee()
914            .await?
915            .ok_or_else(|| ChainError::InactiveChain(block.chain_id))?
916            .1
917            .policy()
918            .clone();
919
920        let mut resource_controller = ResourceController::new(
921            Arc::new(committee_policy),
922            ResourceTracker::default(),
923            block.authenticated_signer,
924        );
925
926        for blob in published_blobs {
927            let blob_id = blob.id();
928            resource_controller
929                .policy()
930                .check_blob_size(blob.content())
931                .with_execution_context(ChainExecutionContext::Block)?;
932            chain.system.used_blobs.insert(&blob_id)?;
933        }
934
935        let mut block_execution_tracker = BlockExecutionTracker::new(
936            &mut resource_controller,
937            published_blobs
938                .iter()
939                .map(|blob| (blob.id(), blob))
940                .collect(),
941            local_time,
942            replaying_oracle_responses,
943            block,
944            phase,
945        )?;
946
947        // Extract failure-policy parameters from exec_policy.
948        let (max_failures, never_reject_application_ids) = match &exec_policy.on_failure {
949            BundleFailurePolicy::Abort => (0, Arc::new(HashSet::new())),
950            BundleFailurePolicy::AutoRetry {
951                max_failures,
952                never_reject_application_ids,
953            } => (*max_failures, never_reject_application_ids.clone()),
954        };
955        let auto_retry = !matches!(exec_policy.on_failure, BundleFailurePolicy::Abort);
956        let mut failure_count = 0u32;
957        let mut never_reject_discarded_origins = HashSet::new();
958
959        // Track cumulative bundle execution time if time budget is set.
960        let time_budget = exec_policy.time_budget;
961        let mut cumulative_bundle_time = Duration::ZERO;
962
963        let mut i = 0;
964        while i < block.transactions.len() {
965            let transaction = &mut block.transactions[i];
966            let is_bundle = matches!(transaction, Transaction::ReceiveMessages(_));
967
968            // Check if time budget has been exceeded for bundles.
969            if is_bundle && time_budget.is_some_and(|budget| cumulative_bundle_time >= budget) {
970                info!(
971                    ?cumulative_bundle_time,
972                    ?time_budget,
973                    "Time budget exceeded, discarding all remaining message bundles"
974                );
975                Self::discard_remaining_bundles(block, i, None);
976                continue;
977            }
978
979            // Checkpoint before bundle transactions if using auto-retry.
980            let checkpoint = if auto_retry && is_bundle {
981                Some((
982                    chain.clone_unchecked()?,
983                    block_execution_tracker.create_checkpoint(),
984                ))
985            } else {
986                None
987            };
988
989            // Track time for bundle execution when time budget is set.
990            let bundle_start = if is_bundle && time_budget.is_some() {
991                Some(Instant::now())
992            } else {
993                None
994            };
995
996            let result = block_execution_tracker
997                .execute_transaction(&*transaction, round, chain)
998                .await;
999
1000            // Update cumulative bundle time.
1001            if let Some(start) = bundle_start {
1002                cumulative_bundle_time += start.elapsed();
1003            }
1004
1005            // If the transaction executed successfully, we move on to the next one.
1006            // On transient errors (e.g. missing blobs) we fail, so it can be retried after
1007            // syncing. In auto-retry mode, we can discard or reject message bundles that failed
1008            // with non-transient errors.
1009            let (error, context, incoming_bundle, saved_chain, saved_tracker) =
1010                match (result, transaction, checkpoint) {
1011                    (Ok(()), _, _) => {
1012                        i += 1;
1013                        continue;
1014                    }
1015                    (
1016                        Err(ChainError::ExecutionError(error, context)),
1017                        Transaction::ReceiveMessages(incoming_bundle),
1018                        Some((saved_chain, saved_tracker)),
1019                    ) if !error.is_transient_error() => {
1020                        (error, context, incoming_bundle, saved_chain, saved_tracker)
1021                    }
1022                    (Err(e), _, _) => return Err(e),
1023                };
1024
1025            // Restore checkpoint.
1026            *chain = saved_chain;
1027            block_execution_tracker.restore_checkpoint(&saved_tracker);
1028
1029            let all_messages_never_reject = !never_reject_application_ids.is_empty()
1030                && incoming_bundle.messages().all(|posted_msg| {
1031                    never_reject_application_ids.contains(&posted_msg.message.application_id())
1032                });
1033            if error.is_limit_error() && i > 0 {
1034                failure_count += 1;
1035                // If we've exceeded max failures, discard all remaining message bundles.
1036                let maybe_sender = if failure_count > max_failures {
1037                    info!(
1038                        failure_count,
1039                        max_failures,
1040                        "Exceeded max bundle failures, discarding all remaining message bundles"
1041                    );
1042                    None
1043                } else {
1044                    // Not the first - discard it and same-sender subsequent bundles.
1045                    info!(
1046                        %error,
1047                        index = i,
1048                        origin = %incoming_bundle.origin,
1049                        "Message bundle exceeded block limits and will be discarded for \
1050                        retry in a later block"
1051                    );
1052                    Some(incoming_bundle.origin)
1053                };
1054                Self::discard_remaining_bundles(block, i, maybe_sender);
1055                // Continue without incrementing i (next transaction is now at i).
1056            } else if (all_messages_never_reject || incoming_bundle.bundle.is_protected())
1057                && incoming_bundle.action != MessageAction::Reject
1058            {
1059                let origin = incoming_bundle.origin;
1060                never_reject_discarded_origins.insert(origin);
1061                warn!(
1062                    %error,
1063                    index = i,
1064                    %origin,
1065                    "Message bundle cannot be rejected (protected or never-reject); \
1066                    discarding the bundle (and same-sender subsequent bundles) for retry \
1067                    in a later block"
1068                );
1069                Self::discard_remaining_bundles(block, i, Some(origin));
1070                // Continue without incrementing i (next transaction is now at i).
1071            } else if incoming_bundle.action == MessageAction::Reject {
1072                // Failed rejected bundles fail the block.
1073                return Err(ChainError::ExecutionError(error, context));
1074            } else {
1075                // Reject the bundle: either a non-limit error, or the first bundle
1076                // exceeded limits (and is inherently too large for any block).
1077                info!(
1078                    %error,
1079                    index = i,
1080                    origin = %incoming_bundle.origin,
1081                    "Message bundle failed to execute and will be rejected"
1082                );
1083                incoming_bundle.action = MessageAction::Reject;
1084                // Retry the transaction as rejected (don't increment i).
1085            }
1086        }
1087
1088        // This can only happen if all transactions were incoming bundles that all got discarded
1089        // due to resource limit errors. This is unlikely in practice but theoretically possible.
1090        ensure!(!block.transactions.is_empty(), ChainError::EmptyBlock);
1091
1092        let recipients = block_execution_tracker.recipients();
1093        let heights = previous_message_blocks_view.multi_get(&recipients).await?;
1094        let mut recipient_heights = Vec::new();
1095        let mut indices = Vec::new();
1096        for (height, recipient) in heights.into_iter().zip(recipients) {
1097            if let Some(height) = height {
1098                let index = usize::try_from(height.0).map_err(|_| ArithmeticError::Overflow)?;
1099                indices.push(index);
1100                recipient_heights.push((recipient, height));
1101            }
1102        }
1103        let hashes = confirmed_log.multi_get(indices).await?;
1104        let mut previous_message_blocks = BTreeMap::new();
1105        for (hash, (recipient, height)) in hashes.into_iter().zip(recipient_heights) {
1106            let hash = hash.ok_or_else(|| {
1107                ChainError::CorruptedChainState("missing entry in confirmed_log".into())
1108            })?;
1109            previous_message_blocks.insert(recipient, (hash, height));
1110        }
1111
1112        let streams = block_execution_tracker.event_streams();
1113        let heights = previous_event_blocks_view.multi_get(&streams).await?;
1114        let mut stream_heights = Vec::new();
1115        let mut indices = Vec::new();
1116        for (stream, height) in streams.into_iter().zip(heights) {
1117            if let Some(height) = height {
1118                let index = usize::try_from(height.0).map_err(|_| ArithmeticError::Overflow)?;
1119                indices.push(index);
1120                stream_heights.push((stream, height));
1121            }
1122        }
1123        let hashes = confirmed_log.multi_get(indices).await?;
1124        let mut previous_event_blocks = BTreeMap::new();
1125        for (hash, (stream, height)) in hashes.into_iter().zip(stream_heights) {
1126            let hash = hash.ok_or_else(|| {
1127                ChainError::CorruptedChainState("missing entry in confirmed_log".into())
1128            })?;
1129            previous_event_blocks.insert(stream, (hash, height));
1130        }
1131
1132        let state_hash = {
1133            #[cfg(with_metrics)]
1134            let state_hash_latency =
1135                metrics::STATE_HASH_COMPUTATION_LATENCY.with_label_values(&[phase.into()]);
1136            #[cfg(with_metrics)]
1137            let _hash_latency = state_hash_latency.measure_latency_us();
1138            chain.crypto_hash_mut().await?
1139        };
1140
1141        let (messages, oracle_responses, events, blobs, operation_results, resource_tracker) =
1142            block_execution_tracker.finalize(block.transactions.len());
1143
1144        Ok((
1145            BlockExecutionOutcome {
1146                messages,
1147                previous_message_blocks,
1148                previous_event_blocks,
1149                state_hash,
1150                oracle_responses,
1151                events,
1152                blobs,
1153                operation_results,
1154            },
1155            resource_tracker,
1156            never_reject_discarded_origins,
1157        ))
1158    }
1159
1160    /// Discards all bundles from the given origin (or all if `None`), starting at the given index.
1161    fn discard_remaining_bundles(
1162        block: &mut ProposedBlock,
1163        mut index: usize,
1164        maybe_origin: Option<ChainId>,
1165    ) {
1166        while index < block.transactions.len() {
1167            if matches!(
1168                &block.transactions[index],
1169                Transaction::ReceiveMessages(bundle)
1170                if maybe_origin.is_none_or(|origin| bundle.origin == origin)
1171            ) {
1172                block.transactions.remove(index);
1173            } else {
1174                index += 1;
1175            }
1176        }
1177    }
1178
1179    /// Executes a block with a specified policy for handling bundle failures.
1180    ///
1181    /// This method supports automatic retry with checkpointing when bundles fail:
1182    /// - For limit errors (block too large, fuel exceeded, etc.): the bundle is discarded
1183    ///   so it can be retried in a later block, unless it's the first transaction
1184    ///   (which gets rejected as inherently too large).
1185    /// - For non-limit errors: the bundle is rejected (triggering bounced messages).
1186    /// - After `max_failures` failed bundles, all remaining message bundles are discarded.
1187    ///
1188    /// The block may be modified to reflect the actual executed transactions.
1189    #[instrument(skip_all, fields(
1190        chain_id = %self.chain_id(),
1191        block_height = %block.height
1192    ))]
1193    pub async fn execute_block(
1194        &mut self,
1195        mut block: ProposedBlock,
1196        local_time: Timestamp,
1197        round: Option<u32>,
1198        published_blobs: &[Blob],
1199        execution: BlockExecution,
1200    ) -> Result<
1201        (
1202            ProposedBlock,
1203            BlockExecutionOutcome,
1204            ResourceTracker,
1205            HashSet<ChainId>,
1206        ),
1207        ChainError,
1208    > {
1209        assert_eq!(
1210            block.chain_id,
1211            self.execution_state.context().extra().chain_id()
1212        );
1213
1214        self.initialize_if_needed(local_time).await?;
1215
1216        let chain_timestamp = *self.execution_state.system.timestamp.get();
1217        ensure!(
1218            chain_timestamp <= block.timestamp,
1219            ChainError::InvalidBlockTimestamp {
1220                parent: chain_timestamp,
1221                new: block.timestamp
1222            }
1223        );
1224        ensure!(!block.transactions.is_empty(), ChainError::EmptyBlock);
1225
1226        ensure!(
1227            block.published_blob_ids()
1228                == published_blobs
1229                    .iter()
1230                    .map(|blob| blob.id())
1231                    .collect::<BTreeSet<_>>(),
1232            ChainError::InternalError("published_blobs mismatch".to_string())
1233        );
1234
1235        if *self.execution_state.system.closed.get() {
1236            ensure!(block.has_only_rejected_messages(), ChainError::ClosedChain);
1237        }
1238
1239        let mandatory_apps_need_accepted_message = self
1240            .current_committee()
1241            .await?
1242            .1
1243            .policy()
1244            .http_request_allow_list
1245            .contains(FLAG_MANDATORY_APPS_NEED_ACCEPTED_MESSAGE);
1246        Self::check_app_permissions(
1247            self.execution_state
1248                .system
1249                .application_permissions
1250                .get()
1251                .await?,
1252            &block,
1253            mandatory_apps_need_accepted_message,
1254        )?;
1255
1256        let phase = execution.phase();
1257        let (replaying_oracle_responses, policy) = execution.into_oracle_and_policy();
1258        Self::execute_block_inner(
1259            &mut self.execution_state,
1260            &self.confirmed_log,
1261            &self.previous_message_blocks,
1262            &self.previous_event_blocks,
1263            &mut block,
1264            local_time,
1265            round,
1266            published_blobs,
1267            replaying_oracle_responses,
1268            policy,
1269            phase,
1270        )
1271        .await
1272        .map(|(outcome, tracker, never_reject_origins)| {
1273            (block, outcome, tracker, never_reject_origins)
1274        })
1275    }
1276
1277    /// Tracks emitted events per stream and returns the set of streams where new contiguous
1278    /// events were observed (starting from `next_expected_events`).
1279    ///
1280    /// Callers must ensure that `next_expected_events` has been initialized for every stream
1281    /// present in the block's events before calling this method. See
1282    /// `ChainWorkerState::initialize_next_expected_events`.
1283    async fn process_emitted_events(
1284        &mut self,
1285        block: &Block,
1286    ) -> Result<BTreeSet<StreamId>, ChainError> {
1287        let mut emitted_streams = BTreeMap::<StreamId, BTreeSet<u32>>::new();
1288        for event in block.body.events.iter().flatten() {
1289            emitted_streams
1290                .entry(event.stream_id.clone())
1291                .or_default()
1292                .insert(event.index);
1293        }
1294
1295        let mut updated_streams = BTreeSet::new();
1296        for (stream_id, indices) in emitted_streams {
1297            // Epoch 0 is created at genesis, so the first published event is index 1.
1298            let initial_index = if stream_id == StreamId::system(EPOCH_STREAM_NAME) {
1299                1
1300            } else {
1301                0
1302            };
1303            let mut current_expected_index = self
1304                .next_expected_events
1305                .get(&stream_id)
1306                .await?
1307                .unwrap_or(initial_index);
1308            for index in indices {
1309                if index == current_expected_index {
1310                    updated_streams.insert(stream_id.clone());
1311                    current_expected_index = index.saturating_add(1);
1312                }
1313            }
1314            if current_expected_index != 0 {
1315                self.next_expected_events
1316                    .insert(&stream_id, current_expected_index)?;
1317            }
1318        }
1319        Ok(updated_streams)
1320    }
1321
1322    /// Applies an execution outcome to the chain, updating the outboxes, state hash and chain
1323    /// manager. This does not touch the execution state itself, which must be updated separately.
1324    /// Returns the set of event streams that were updated as a result of applying the block.
1325    #[instrument(skip_all, fields(
1326        chain_id = %self.chain_id(),
1327        block_height = %block.inner().inner().header.height
1328    ))]
1329    pub async fn apply_confirmed_block(
1330        &mut self,
1331        block: &ConfirmedBlock,
1332        local_time: Timestamp,
1333        tracked: Option<&ChainIdSet>,
1334    ) -> Result<BTreeSet<StreamId>, ChainError> {
1335        let hash = block.inner().hash();
1336        let block = block.inner().inner();
1337        if block.header.height == BlockHeight::ZERO {
1338            self.block_zero_executed_at.set(local_time);
1339        }
1340        self.execution_state_hash.set(Some(block.header.state_hash));
1341        let recipients = self.process_outgoing_messages(block, tracked).await?;
1342
1343        for recipient in recipients {
1344            self.previous_message_blocks
1345                .insert(&recipient, block.header.height)?;
1346        }
1347        for event in block.body.events.iter().flatten() {
1348            self.previous_event_blocks
1349                .insert(&event.stream_id, block.header.height)?;
1350        }
1351        let updated_streams = self.process_emitted_events(block).await?;
1352        // Last, reset the consensus state based on the current ownership.
1353        self.reset_chain_manager(block.header.height.try_add_one()?, local_time)
1354            .await?;
1355
1356        // Advance to next block height.
1357        let tip = self.tip_state.get_mut();
1358        tip.block_hash = Some(hash);
1359        tip.next_block_height.try_add_assign_one()?;
1360        tip.update_counters(&block.body.transactions, &block.body.messages)?;
1361        self.confirmed_log.push(hash);
1362        self.preprocessed_blocks.remove(&block.header.height)?;
1363        Ok(updated_streams)
1364    }
1365
1366    /// Adds a block to `preprocessed_blocks`, and updates the outboxes where possible.
1367    /// Returns the set of event streams that were updated as a result of preprocessing the block.
1368    #[instrument(skip_all, fields(
1369        chain_id = %self.chain_id(),
1370        block_height = %block.inner().inner().header.height
1371    ))]
1372    pub async fn preprocess_block(
1373        &mut self,
1374        block: &ConfirmedBlock,
1375        tracked: Option<&ChainIdSet>,
1376    ) -> Result<BTreeSet<StreamId>, ChainError> {
1377        let hash = block.inner().hash();
1378        let block = block.inner().inner();
1379        let height = block.header.height;
1380        if height < self.tip_state.get().next_block_height {
1381            return Ok(BTreeSet::new());
1382        }
1383        self.process_outgoing_messages(block, tracked).await?;
1384        let updated_streams = self.process_emitted_events(block).await?;
1385        self.preprocessed_blocks.insert(&height, hash)?;
1386        Ok(updated_streams)
1387    }
1388
1389    /// Verifies that the block is valid according to the chain's application permission settings.
1390    #[instrument(skip_all, fields(
1391        block_height = %block.height,
1392        num_transactions = %block.transactions.len()
1393    ))]
1394    fn check_app_permissions(
1395        app_permissions: &ApplicationPermissions,
1396        block: &ProposedBlock,
1397        mandatory_apps_need_accepted_message: bool,
1398    ) -> Result<(), ChainError> {
1399        let mut mandatory = app_permissions
1400            .mandatory_applications
1401            .iter()
1402            .copied()
1403            .collect::<HashSet<ApplicationId>>();
1404        for transaction in &block.transactions {
1405            match transaction {
1406                Transaction::ExecuteOperation(operation)
1407                    if operation.is_exempt_from_permissions() =>
1408                {
1409                    mandatory.clear()
1410                }
1411                Transaction::ExecuteOperation(operation) => {
1412                    ensure!(
1413                        app_permissions.can_execute_operations(&operation.application_id()),
1414                        ChainError::AuthorizedApplications(
1415                            app_permissions.execute_operations.clone().unwrap()
1416                        )
1417                    );
1418                    if let Operation::User { application_id, .. } = operation {
1419                        mandatory.remove(application_id);
1420                    }
1421                }
1422                Transaction::ReceiveMessages(incoming_bundle)
1423                    if !mandatory_apps_need_accepted_message
1424                        || incoming_bundle.action == MessageAction::Accept =>
1425                {
1426                    for pending in incoming_bundle.messages() {
1427                        if let Message::User { application_id, .. } = &pending.message {
1428                            mandatory.remove(application_id);
1429                        }
1430                    }
1431                }
1432                Transaction::ReceiveMessages(_) => {}
1433            }
1434        }
1435        ensure!(
1436            mandatory.is_empty(),
1437            ChainError::MissingMandatoryApplications(mandatory.into_iter().collect())
1438        );
1439        Ok(())
1440    }
1441
1442    /// Returns the hashes of all blocks we have in the given range.
1443    ///
1444    /// If the input heights are in ascending order, the hashes will be in the same order.
1445    /// Otherwise they may be unordered.
1446    #[instrument(skip_all, fields(
1447        chain_id = %self.chain_id(),
1448        next_block_height = %self.tip_state.get().next_block_height,
1449    ))]
1450    pub async fn block_hashes(
1451        &self,
1452        heights: impl IntoIterator<Item = BlockHeight>,
1453    ) -> Result<Vec<CryptoHash>, ChainError> {
1454        let next_height = self.tip_state.get().next_block_height;
1455        // Everything up to (excluding) next_height is in confirmed_log.
1456        let (confirmed_heights, unconfirmed_heights) = heights
1457            .into_iter()
1458            .partition::<Vec<_>, _>(|height| *height < next_height);
1459        let confirmed_indices = confirmed_heights
1460            .into_iter()
1461            .map(|height| usize::try_from(height.0).map_err(|_| ArithmeticError::Overflow))
1462            .collect::<Result<_, _>>()?;
1463        let confirmed_hashes = self.confirmed_log.multi_get(confirmed_indices).await?;
1464        // Everything after (including) next_height in preprocessed_blocks if we have it.
1465        let unconfirmed_hashes = self
1466            .preprocessed_blocks
1467            .multi_get(&unconfirmed_heights)
1468            .await?;
1469        Ok(confirmed_hashes
1470            .into_iter()
1471            .chain(unconfirmed_hashes)
1472            .flatten()
1473            .collect())
1474    }
1475
1476    /// Resets the chain manager for the next block height.
1477    async fn reset_chain_manager(
1478        &mut self,
1479        next_height: BlockHeight,
1480        local_time: Timestamp,
1481    ) -> Result<(), ChainError> {
1482        let maybe_committee = self.execution_state.system.current_committee().await?;
1483        let ownership = self.execution_state.system.ownership.get().await?.clone();
1484        let fallback_owners = maybe_committee
1485            .iter()
1486            .flat_map(|(_, committee)| committee.account_keys_and_weights());
1487        self.pending_validated_blobs.clear();
1488        self.pending_proposed_blobs.clear();
1489        self.manager
1490            .reset(ownership, next_height, local_time, fallback_owners)
1491    }
1492
1493    /// Updates the outboxes with the messages sent in the block.
1494    ///
1495    /// Returns the set of all recipients.
1496    #[instrument(skip_all, fields(
1497        chain_id = %self.chain_id(),
1498        block_height = %block.header.height
1499    ))]
1500    async fn process_outgoing_messages(
1501        &mut self,
1502        block: &Block,
1503        tracked: Option<&ChainIdSet>,
1504    ) -> Result<Vec<ChainId>, ChainError> {
1505        // Record the messages of the execution. Messages are understood within an
1506        // application.
1507        let recipients = block.recipients();
1508        let block_height = block.header.height;
1509        let next_height = self.tip_state.get().next_block_height;
1510
1511        // Update the outboxes. Every recipient's per-target outbox queue is updated, but the
1512        // `nonempty_outboxes` and `outbox_counters` indices are only populated for targets we track.
1513        let targets = recipients.into_iter().collect::<Vec<_>>();
1514        let outboxes = self.outboxes.try_load_entries_mut(&targets).await?;
1515        let mut scheduled_tracked = Vec::new();
1516        for (mut outbox, target) in outboxes.into_iter().zip(&targets) {
1517            if block_height > next_height {
1518                // There may be a gap in the chain before this block. We can only add it to this
1519                // outbox if the previous message to the same recipient has already been added.
1520                if *outbox.next_height_to_schedule.get() > block_height {
1521                    continue; // We already added this recipient's messages to the outbox.
1522                }
1523                let maybe_prev_hash = match outbox.next_height_to_schedule.get().try_sub_one().ok()
1524                {
1525                    // The block with the last added message has already been executed; look up its
1526                    // hash in the confirmed_log.
1527                    Some(height) if height < next_height => {
1528                        let index =
1529                            usize::try_from(height.0).map_err(|_| ArithmeticError::Overflow)?;
1530                        Some(self.confirmed_log.get(index).await?.ok_or_else(|| {
1531                            ChainError::CorruptedChainState("missing entry in confirmed_log".into())
1532                        })?)
1533                    }
1534                    // The block with last added message has not been executed yet. If we have it,
1535                    // it's in preprocessed_blocks.
1536                    Some(height) => Some(self.preprocessed_blocks.get(&height).await?.ok_or_else(
1537                        || {
1538                            ChainError::CorruptedChainState(
1539                                "missing entry in preprocessed_blocks".into(),
1540                            )
1541                        },
1542                    )?),
1543                    None => None, // No message to that sender was added yet.
1544                };
1545                // Only schedule if this block contains the next message for that recipient.
1546                match (
1547                    maybe_prev_hash,
1548                    block.body.previous_message_blocks.get(target),
1549                ) {
1550                    (None, None) => {
1551                        // No previous message block expected and none indicated by the outbox -
1552                        // all good
1553                    }
1554                    (Some(_), None) => {
1555                        // Outbox indicates there was a previous message block, but
1556                        // previous_message_blocks has no idea about it - possible bug
1557                        return Err(ChainError::CorruptedChainState(
1558                            "block indicates no previous message block,\
1559                            but we have one in the outbox"
1560                                .into(),
1561                        ));
1562                    }
1563                    (None, Some((_, prev_msg_block_height))) => {
1564                        // We have no previously processed block in the outbox, but we are
1565                        // expecting one - this could be due to an empty outbox having been pruned.
1566                        // Only process the outbox if the height of the previous message block is
1567                        // lower than the tip
1568                        if *prev_msg_block_height >= next_height {
1569                            continue;
1570                        }
1571                    }
1572                    (Some(ref prev_hash), Some((prev_msg_block_hash, _))) => {
1573                        // Only process the outbox if the hashes match.
1574                        if prev_hash != prev_msg_block_hash {
1575                            continue;
1576                        }
1577                    }
1578                }
1579            }
1580            if outbox.schedule_message(block_height)?
1581                && tracked.is_none_or(|set| set.contains(target))
1582            {
1583                scheduled_tracked.push(*target);
1584            }
1585            #[cfg(with_metrics)]
1586            crate::outbox::metrics::OUTBOX_SIZE
1587                .with_label_values(&[])
1588                .observe(outbox.queue.count() as f64);
1589        }
1590
1591        if !scheduled_tracked.is_empty() {
1592            // All scheduled messages are at `block_height`.
1593            *self
1594                .outbox_counters
1595                .get_mut()
1596                .entry(block_height)
1597                .or_default() += scheduled_tracked.len() as u32;
1598            let nonempty_outboxes = self.nonempty_outboxes.get_mut();
1599            for target in &scheduled_tracked {
1600                nonempty_outboxes.insert(*target);
1601            }
1602        }
1603
1604        #[cfg(with_metrics)]
1605        metrics::NUM_OUTBOXES
1606            .with_label_values(&[])
1607            .observe(self.nonempty_outboxes.get().len() as f64);
1608        #[cfg(with_metrics)]
1609        metrics::OUTBOX_COUNTERS_SIZE
1610            .with_label_values(&[])
1611            .observe(self.outbox_counters.get().len() as f64);
1612        Ok(targets)
1613    }
1614}
1615
1616#[test]
1617fn empty_block_size() {
1618    let size = bcs::serialized_size(&crate::block::Block::new(
1619        crate::test::make_first_block(
1620            linera_execution::test_utils::dummy_chain_description(0).id(),
1621        ),
1622        crate::data_types::BlockExecutionOutcome::default(),
1623    ))
1624    .unwrap();
1625    assert_eq!(size, EMPTY_BLOCK_SIZE);
1626}