1use 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, Cursor,
14 Epoch, NonCanonicalBTreeMap, NonCanonicalBTreeSet, OracleResponse, Timestamp,
15 },
16 ensure,
17 hashed::Hashed,
18 identifiers::{AccountOwner, ApplicationId, BlobType, ChainId, GenericApplicationId, StreamId},
19 ownership::ChainOwnership,
20 time::{Duration, Instant},
21};
22use linera_execution::{
23 committee::Committee, ExecutionRuntimeContext, ExecutionStateView, Message, Operation,
24 PreparedCheckpoint, Query, QueryContext, QueryOutcome, ResourceController, ResourceTracker,
25 ServiceRuntimeEndpoint, TransactionTracker,
26};
27use linera_views::{
28 context::Context,
29 log_view::LogView,
30 map_view::{CustomMapView, MapView},
31 reentrant_collection_view::{ReadGuardedView, ReentrantCollectionView},
32 register_view::RegisterView,
33 set_view::SetView,
34 views::{ClonableView, RootView, View},
35};
36use serde::{Deserialize, Serialize};
37use tracing::{info, instrument, warn};
38
39use crate::{
40 block::{Block, ConfirmedBlock},
41 block_tracker::BlockExecutionTracker,
42 data_types::{
43 BlockExecutionOutcome, BundleExecutionPolicy, BundleFailurePolicy, ChainAndHeight,
44 IncomingBundle, MessageAction, MessageBundle, ProposedBlock, Transaction,
45 },
46 inbox::{InboxError, InboxStateView},
47 manager::ChainManager,
48 outbox::OutboxStateView,
49 pending_blobs::PendingBlobsView,
50 ChainError, ChainExecutionContext, ExecutionError, ExecutionResultExt,
51};
52
53#[cfg(test)]
54#[path = "unit_tests/chain_tests.rs"]
55mod chain_tests;
56
57#[cfg(with_metrics)]
58use linera_base::prometheus_util::MeasureLatency;
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum BlockExecutionPhase {
70 StageProposal,
72 HandleProposal,
74 HandleConfirmed,
77}
78
79impl BlockExecutionPhase {
80 pub fn as_str(self) -> &'static str {
82 match self {
83 BlockExecutionPhase::StageProposal => "stage_proposal",
84 BlockExecutionPhase::HandleProposal => "handle_proposal",
85 BlockExecutionPhase::HandleConfirmed => "handle_confirmed",
86 }
87 }
88}
89
90#[cfg(with_metrics)]
91pub(crate) mod metrics {
92 use std::sync::LazyLock;
93
94 use linera_base::prometheus_util::{
95 exponential_bucket_interval, register_histogram_vec, register_int_counter_vec,
96 };
97 use linera_execution::ResourceTracker;
98 use prometheus::{HistogramVec, IntCounterVec};
99
100 pub static NUM_BLOCKS_EXECUTED: LazyLock<IntCounterVec> = LazyLock::new(|| {
101 register_int_counter_vec(
102 "num_blocks_executed",
103 "Number of blocks executed",
104 &["phase"],
105 )
106 });
107
108 pub static BLOCK_EXECUTION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
109 register_histogram_vec(
110 "block_execution_latency",
111 "Block execution latency",
112 &["phase"],
113 exponential_bucket_interval(50.0_f64, 10_000_000.0),
114 )
115 });
116
117 #[cfg(with_metrics)]
118 pub static MESSAGE_EXECUTION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
119 register_histogram_vec(
120 "message_execution_latency",
121 "Message execution latency",
122 &["phase"],
123 exponential_bucket_interval(0.1_f64, 50_000.0),
124 )
125 });
126
127 pub static OPERATION_EXECUTION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
128 register_histogram_vec(
129 "operation_execution_latency",
130 "Operation execution latency",
131 &["phase"],
132 exponential_bucket_interval(0.1_f64, 50_000.0),
133 )
134 });
135
136 pub static WASM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
137 register_histogram_vec(
138 "wasm_fuel_used_per_block",
139 "Wasm fuel used per block",
140 &["phase"],
141 exponential_bucket_interval(10.0, 100_000_000.0),
142 )
143 });
144
145 pub static EVM_FUEL_USED_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
146 register_histogram_vec(
147 "evm_fuel_used_per_block",
148 "EVM fuel used per block",
149 &["phase"],
150 exponential_bucket_interval(10.0, 100_000_000.0),
151 )
152 });
153
154 pub static VM_NUM_READS_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
155 register_histogram_vec(
156 "vm_num_reads_per_block",
157 "VM number of reads per block",
158 &["phase"],
159 exponential_bucket_interval(0.1, 100.0),
160 )
161 });
162
163 pub static VM_BYTES_READ_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
164 register_histogram_vec(
165 "vm_bytes_read_per_block",
166 "VM number of bytes read per block",
167 &["phase"],
168 exponential_bucket_interval(0.1, 10_000_000.0),
169 )
170 });
171
172 pub static VM_BYTES_WRITTEN_PER_BLOCK: LazyLock<HistogramVec> = LazyLock::new(|| {
173 register_histogram_vec(
174 "vm_bytes_written_per_block",
175 "VM number of bytes written per block",
176 &["phase"],
177 exponential_bucket_interval(0.1, 10_000_000.0),
178 )
179 });
180
181 pub static STATE_HASH_COMPUTATION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
182 register_histogram_vec(
183 "state_hash_computation_latency",
184 "Time to recompute the state hash, in microseconds",
185 &["phase"],
186 exponential_bucket_interval(1.0, 2_000_000.0),
187 )
188 });
189
190 pub static NUM_OUTBOXES: LazyLock<HistogramVec> = LazyLock::new(|| {
191 register_histogram_vec(
192 "num_outboxes",
193 "Number of outboxes",
194 &[],
195 exponential_bucket_interval(1.0, 1_000_000.0),
196 )
197 });
198
199 pub static OUTBOX_COUNTERS_SIZE: LazyLock<HistogramVec> = LazyLock::new(|| {
200 register_histogram_vec(
201 "outbox_counters_size",
202 "Number of entries in the outbox_counters map (in-flight message heights)",
203 &[],
204 exponential_bucket_interval(1.0, 1_000_000.0),
205 )
206 });
207
208 pub(crate) fn track_block_metrics(
210 tracker: &ResourceTracker,
211 phase: super::BlockExecutionPhase,
212 ) {
213 let phase = &[phase.as_str()];
214 NUM_BLOCKS_EXECUTED.with_label_values(phase).inc();
215 WASM_FUEL_USED_PER_BLOCK
216 .with_label_values(phase)
217 .observe(tracker.wasm_fuel as f64);
218 EVM_FUEL_USED_PER_BLOCK
219 .with_label_values(phase)
220 .observe(tracker.evm_fuel as f64);
221 VM_NUM_READS_PER_BLOCK
222 .with_label_values(phase)
223 .observe(tracker.read_operations as f64);
224 VM_BYTES_READ_PER_BLOCK
225 .with_label_values(phase)
226 .observe(tracker.bytes_read as f64);
227 VM_BYTES_WRITTEN_PER_BLOCK
228 .with_label_values(phase)
229 .observe(tracker.bytes_written as f64);
230 }
231}
232
233pub(crate) const EMPTY_BLOCK_SIZE: usize = 94;
235
236#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
242pub struct ChainIdSet(pub BTreeSet<ChainId>);
243
244impl linera_base::crypto::BcsHashable<'_> for ChainIdSet {}
245
246impl std::ops::Deref for ChainIdSet {
247 type Target = BTreeSet<ChainId>;
248
249 fn deref(&self) -> &Self::Target {
250 &self.0
251 }
252}
253
254#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Allocative)]
257#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
258pub struct StreamCounts {
259 pub first_index: u32,
264 pub next_index: u32,
267}
268
269#[cfg_attr(
271 with_graphql,
272 derive(async_graphql::SimpleObject),
273 graphql(cache_control(no_cache))
274)]
275#[derive(Debug, RootView, ClonableView, Allocative)]
276#[allocative(bound = "C")]
277pub struct ChainStateView<C>
278where
279 C: Clone + Context + 'static,
280{
281 pub execution_state: ExecutionStateView<C>,
283
284 pub tip_state: RegisterView<C, ChainTipState>,
286
287 pub manager: ChainManager<C>,
289 pub pending_validated_blobs: PendingBlobsView<C>,
292 pub pending_proposed_blobs: ReentrantCollectionView<C, AccountOwner, PendingBlobsView<C>>,
294
295 pub block_hashes: CustomMapView<C, BlockHeight, CryptoHash>,
299 pub received_log: LogView<C, ChainAndHeight>,
301 pub received_certificate_trackers: RegisterView<C, HashMap<ValidatorPublicKey, u64>>,
303
304 pub inboxes: ReentrantCollectionView<C, ChainId, InboxStateView<C>>,
306 pub outboxes: ReentrantCollectionView<C, ChainId, OutboxStateView<C>>,
308 pub next_expected_events: MapView<C, StreamId, StreamCounts>,
312 pub outbox_counters: RegisterView<C, NonCanonicalBTreeMap<BlockHeight, u32>>,
315 pub nonempty_outboxes: RegisterView<C, NonCanonicalBTreeSet<ChainId>>,
317
318 pub nonempty_inboxes: RegisterView<C, NonCanonicalBTreeSet<ChainId>>,
320
321 pub chain_initialized_at: RegisterView<C, Timestamp>,
326
327 pub next_height_to_preprocess: RegisterView<C, BlockHeight>,
336
337 pub latest_checkpoint_height: RegisterView<C, Option<BlockHeight>>,
341
342 pub pre_checkpoint_block_trust: SetView<C, CryptoHash>,
350
351 pub outbox_index_tracked_hash: RegisterView<C, Option<CryptoHash>>,
358}
359
360#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
362#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
363pub struct ChainTipState {
364 pub block_hash: Option<CryptoHash>,
366 pub next_block_height: BlockHeight,
368}
369
370impl ChainTipState {
371 pub fn verify_block_chaining(&self, new_block: &ProposedBlock) -> Result<(), ChainError> {
374 ensure!(
375 new_block.height == self.next_block_height,
376 ChainError::UnexpectedBlockHeight {
377 expected_block_height: self.next_block_height,
378 found_block_height: new_block.height
379 }
380 );
381 ensure!(
382 new_block.previous_block_hash == self.block_hash,
383 ChainError::UnexpectedPreviousBlockHash
384 );
385 Ok(())
386 }
387
388 pub fn already_validated_block(&self, height: BlockHeight) -> Result<bool, ChainError> {
391 ensure!(
392 self.next_block_height >= height,
393 ChainError::MissingEarlierBlocks {
394 current_block_height: self.next_block_height,
395 }
396 );
397 Ok(self.next_block_height > height)
398 }
399}
400
401impl<C> ChainStateView<C>
402where
403 C: Context + Clone + 'static,
404 C::Extra: ExecutionRuntimeContext,
405{
406 pub fn chain_id(&self) -> ChainId {
408 self.context().extra().chain_id()
409 }
410
411 #[instrument(skip_all, fields(
412 chain_id = %self.chain_id(),
413 ))]
414 pub async fn query_application(
416 &mut self,
417 local_time: Timestamp,
418 query: Query,
419 service_runtime_endpoint: Option<&mut ServiceRuntimeEndpoint>,
420 ) -> Result<QueryOutcome, ChainError> {
421 let context = QueryContext {
422 chain_id: self.chain_id(),
423 next_block_height: self.tip_state.get().next_block_height,
424 local_time,
425 };
426 self.execution_state
427 .query_application(context, query, service_runtime_endpoint)
428 .await
429 .with_execution_context(ChainExecutionContext::Query)
430 }
431
432 #[instrument(skip_all, fields(
433 chain_id = %self.chain_id(),
434 application_id = %application_id
435 ))]
436 pub async fn describe_application(
438 &mut self,
439 application_id: ApplicationId,
440 ) -> Result<ApplicationDescription, ChainError> {
441 self.execution_state
442 .system
443 .describe_application(application_id, &mut TransactionTracker::default())
444 .await
445 .with_execution_context(ChainExecutionContext::DescribeApplication)
446 }
447
448 #[instrument(skip_all, fields(
449 chain_id = %self.chain_id(),
450 target = %target,
451 height = %height
452 ))]
453 pub async fn mark_messages_as_received(
456 &mut self,
457 target: &ChainId,
458 height: BlockHeight,
459 tracked: Option<&ChainIdSet>,
460 ) -> Result<bool, ChainError> {
461 let mut outbox = self.outboxes.try_load_entry_mut(target).await?;
462 let updates = outbox.mark_messages_as_received(height).await?;
463 if updates.is_empty() {
464 return Ok(false);
465 }
466 if tracked.is_none_or(|tracked| tracked.contains(target)) {
473 for update in updates {
474 let counter = self
475 .outbox_counters
476 .get_mut()
477 .get_mut(&update)
478 .ok_or_else(|| {
479 ChainError::CorruptedChainState("message counter should be present".into())
480 })?;
481 *counter = counter.checked_sub(1).ok_or(ArithmeticError::Underflow)?;
482 if *counter == 0 {
483 self.outbox_counters.get_mut().remove(&update);
485 }
486 }
487 }
488 if outbox.queue.count() == 0 {
489 self.nonempty_outboxes.get_mut().remove(target);
490 if *outbox.next_height_to_schedule.get() <= self.tip_state.get().next_block_height {
492 self.outboxes.remove_entry(target)?;
493 }
494 }
495 #[cfg(with_metrics)]
496 metrics::NUM_OUTBOXES
497 .with_label_values(&[])
498 .observe(self.nonempty_outboxes.get().len() as f64);
499 #[cfg(with_metrics)]
500 metrics::OUTBOX_COUNTERS_SIZE
501 .with_label_values(&[])
502 .observe(self.outbox_counters.get().len() as f64);
503 Ok(true)
504 }
505
506 pub fn all_messages_delivered_up_to(&self, height: BlockHeight) -> bool {
509 tracing::debug!(
510 "Messages left in {:.8}'s outbox: {:?}",
511 self.chain_id(),
512 self.outbox_counters.get()
513 );
514 if let Some((key, _)) = self.outbox_counters.get().first_key_value() {
515 key > &height
516 } else {
517 true
518 }
519 }
520
521 pub async fn is_active(&self) -> Result<bool, ChainError> {
523 Ok(self.execution_state.system.is_active().await?)
524 }
525
526 pub async fn initialize_if_needed(&mut self, local_time: Timestamp) -> Result<(), ChainError> {
528 let chain_id = self.chain_id();
529 if self
531 .execution_state
532 .system
533 .initialize_chain(chain_id)
534 .await
535 .with_execution_context(ChainExecutionContext::Block)?
536 {
537 return Ok(());
539 }
540 let maybe_committee = self
541 .execution_state
542 .system
543 .current_committee()
544 .await
545 .with_execution_context(ChainExecutionContext::Block)?;
546 self.manager.reset(
548 self.execution_state.system.ownership.get().await?.clone(),
549 BlockHeight(0),
550 local_time,
551 maybe_committee
552 .iter()
553 .flat_map(|(_, committee)| committee.account_keys_and_weights()),
554 )?;
555 Ok(())
556 }
557
558 fn insert_block_hash(
562 &mut self,
563 height: BlockHeight,
564 hash: CryptoHash,
565 ) -> Result<(), ChainError> {
566 self.block_hashes.insert(&height, hash)?;
567 let next = self.next_height_to_preprocess.get_mut();
568 if *next <= height {
569 *next = height.try_add_one()?;
570 }
571 Ok(())
572 }
573
574 #[instrument(skip_all, fields(
581 chain_id = %self.chain_id(),
582 origin = %origin,
583 bundle_height = %bundle.height
584 ))]
585 pub async fn receive_message_bundle_with_inbox(
586 &mut self,
587 inbox: &mut InboxStateView<C>,
588 origin: &ChainId,
589 bundle: MessageBundle,
590 local_time: Timestamp,
591 add_to_received_log: bool,
592 ) -> Result<(), ChainError> {
593 assert!(!bundle.messages.is_empty());
594 let chain_id = self.chain_id();
595 tracing::trace!(
596 "Processing new messages from {origin} at height {}",
597 bundle.height,
598 );
599 let chain_and_height = ChainAndHeight {
600 chain_id: *origin,
601 height: bundle.height,
602 };
603
604 match self.initialize_if_needed(local_time).await {
605 Ok(_) => (),
606 Err(ChainError::ExecutionError(exec_err, _))
609 if matches!(*exec_err, ExecutionError::BlobsNotFound(ref blobs)
610 if blobs.iter().all(|blob_id| {
611 blob_id.blob_type == BlobType::ChainDescription && blob_id.hash == chain_id.0
612 })) => {}
613 err => {
614 return err;
615 }
616 }
617
618 let newly_added = inbox
620 .add_bundle(bundle)
621 .await
622 .map_err(|error| match error {
623 InboxError::ViewError(error) => ChainError::ViewError(error),
624 error => ChainError::CorruptedChainState(format!(
625 "while processing messages in certified block: {error}"
626 )),
627 })?;
628 if newly_added {
629 self.nonempty_inboxes.get_mut().insert(*origin);
630 }
631
632 if add_to_received_log {
634 self.received_log.push(chain_and_height);
635 }
636 Ok(())
637 }
638
639 pub fn update_received_certificate_trackers(
641 &mut self,
642 new_trackers: BTreeMap<ValidatorPublicKey, u64>,
643 ) {
644 for (name, tracker) in new_trackers {
645 self.received_certificate_trackers
646 .get_mut()
647 .entry(name)
648 .and_modify(|t| {
649 if tracker > *t {
652 *t = tracker;
653 }
654 })
655 .or_insert(tracker);
656 }
657 }
658
659 pub async fn current_committee(&self) -> Result<(Epoch, Arc<Committee>), ChainError> {
661 let chain_id = self.chain_id();
662 self.execution_state
663 .system
664 .current_committee()
665 .await
666 .with_execution_context(ChainExecutionContext::Block)?
667 .ok_or(ChainError::InactiveChain(chain_id))
668 }
669
670 pub async fn ownership(&self) -> Result<&ChainOwnership, ChainError> {
672 Ok(self.execution_state.system.ownership.get().await?)
673 }
674
675 #[instrument(skip_all, fields(
681 chain_id = %self.chain_id(),
682 ))]
683 pub async fn remove_bundles_from_inboxes(
684 &mut self,
685 timestamp: Timestamp,
686 must_be_present: bool,
687 incoming_bundles: impl IntoIterator<Item = &IncomingBundle>,
688 ) -> Result<(), ChainError> {
689 let chain_id = self.chain_id();
690 let mut bundles_by_origin: BTreeMap<_, Vec<&MessageBundle>> = Default::default();
691 for IncomingBundle { bundle, origin, .. } in incoming_bundles {
692 ensure!(
693 bundle.timestamp <= timestamp,
694 ChainError::IncorrectBundleTimestamp {
695 chain_id,
696 bundle_timestamp: bundle.timestamp,
697 block_timestamp: timestamp,
698 }
699 );
700 let bundles = bundles_by_origin.entry(*origin).or_default();
701 bundles.push(bundle);
702 }
703 let origins = bundles_by_origin.keys().copied().collect::<Vec<_>>();
704 let inboxes = self.inboxes.try_load_entries_mut(&origins).await?;
705 for ((origin, bundles), mut inbox) in bundles_by_origin.into_iter().zip(inboxes) {
706 tracing::trace!(
707 "Removing [{}] from inbox for {origin}",
708 bundles
709 .iter()
710 .map(|bundle| bundle.height.to_string())
711 .collect::<Vec<_>>()
712 .join(", ")
713 );
714 for bundle in bundles {
715 let was_present = inbox
717 .remove_bundle(bundle)
718 .await
719 .map_err(|error| (chain_id, origin, error))?;
720 if must_be_present {
721 ensure!(
722 was_present,
723 ChainError::MissingCrossChainUpdate {
724 chain_id,
725 origin,
726 height: bundle.height,
727 }
728 );
729 }
730 }
731 inbox.observe_size_metric();
732 if inbox.added_bundles.count() == 0 {
733 self.nonempty_inboxes.get_mut().remove(&origin);
734 }
735 }
736 Ok(())
737 }
738
739 pub fn nonempty_outbox_chain_ids(&self) -> Vec<ChainId> {
741 self.nonempty_outboxes.get().iter().copied().collect()
742 }
743
744 pub async fn load_outboxes(
746 &self,
747 targets: &[ChainId],
748 ) -> Result<Vec<ReadGuardedView<OutboxStateView<C>>>, ChainError> {
749 let vec_of_options = self.outboxes.try_load_entries(targets).await?;
750 let optional_vec = vec_of_options.into_iter().collect::<Option<Vec<_>>>();
751 optional_vec.ok_or_else(|| ChainError::CorruptedChainState("Missing outboxes".into()))
752 }
753
754 pub async fn reconcile_outbox_index(
760 &mut self,
761 tracked: Option<&Hashed<ChainIdSet>>,
762 ) -> Result<bool, ChainError> {
763 if *self.outbox_index_tracked_hash.get() == tracked.map(|tracked| tracked.hash()) {
764 return Ok(false);
765 }
766 self.rebuild_outbox_index(tracked).await?;
767 Ok(true)
768 }
769
770 async fn rebuild_outbox_index(
776 &mut self,
777 tracked: Option<&Hashed<ChainIdSet>>,
778 ) -> Result<(), ChainError> {
779 self.nonempty_outboxes.get_mut().clear();
780 self.outbox_counters.get_mut().clear();
781 let targets = match tracked {
784 Some(tracked) => tracked.inner().iter().copied().collect::<Vec<_>>(),
785 None => self.outboxes.indices().await?,
786 };
787 for target in &targets {
788 let heights = {
789 let Some(outbox) = self.outboxes.try_load_entry(target).await? else {
790 continue;
791 };
792 outbox.queue.elements().await?
793 };
794 if heights.is_empty() {
795 continue;
796 }
797 for height in heights {
798 *self.outbox_counters.get_mut().entry(height).or_default() += 1;
799 }
800 self.nonempty_outboxes.get_mut().insert(*target);
801 }
802 self.outbox_index_tracked_hash
803 .set(tracked.map(|tracked| tracked.hash()));
804 Ok(())
805 }
806
807 pub fn outbox_index_is_reconciled(&self, tracked: Option<&Hashed<ChainIdSet>>) -> bool {
810 *self.outbox_index_tracked_hash.get() == tracked.map(|tracked| tracked.hash())
811 }
812
813 #[expect(clippy::too_many_arguments)]
815 #[instrument(skip_all, fields(
816 chain_id = %block.chain_id,
817 block_height = %block.height
818 ))]
819 async fn execute_block_inner(
820 chain: &mut ExecutionStateView<C>,
821 block_hashes: &CustomMapView<C, BlockHeight, CryptoHash>,
822 block: &mut ProposedBlock,
823 local_time: Timestamp,
824 round: Option<u32>,
825 published_blobs: &[Blob],
826 replaying_oracle_responses: Option<Vec<Vec<OracleResponse>>>,
827 exec_policy: BundleExecutionPolicy,
828 phase: BlockExecutionPhase,
829 checkpoint_origin_cursors: Vec<(ChainId, Cursor)>,
830 checkpoint_inbox_cursors: Vec<(ChainId, Cursor)>,
831 checkpoint_outbox_block_hashes: Vec<CryptoHash>,
832 ) -> Result<(BlockExecutionOutcome, ResourceTracker, HashSet<ChainId>), ChainError> {
833 if !matches!(&exec_policy.on_failure, BundleFailurePolicy::Abort) {
836 assert!(
837 replaying_oracle_responses.is_none(),
838 "Cannot use AutoRetry policy when replaying oracle responses"
839 );
840 }
841
842 #[cfg(with_metrics)]
843 let block_execution_latency =
844 metrics::BLOCK_EXECUTION_LATENCY.with_label_values(&[phase.as_str()]);
845 #[cfg(with_metrics)]
846 let _execution_latency = block_execution_latency.measure_latency_us();
847
848 let committee_policy = chain
853 .system
854 .current_committee()
855 .await
856 .with_execution_context(ChainExecutionContext::Block)?
857 .ok_or_else(|| ChainError::InactiveChain(block.chain_id))?
858 .1
859 .policy()
860 .clone();
861
862 let prepared_checkpoint = if block.starts_with_checkpoint() {
871 let blobs = chain
872 .prepare_checkpoint(committee_policy.maximum_blob_size)
873 .await
874 .with_execution_context(ChainExecutionContext::Block)?;
875 Some(PreparedCheckpoint {
876 blobs,
877 origin_cursors: checkpoint_origin_cursors,
878 inbox_cursors: checkpoint_inbox_cursors,
879 outbox_block_hashes: checkpoint_outbox_block_hashes,
880 })
881 } else {
882 None
883 };
884
885 chain.system.progress.get_mut().timestamp = block.timestamp;
886
887 let start_epoch = *chain.system.epoch.get();
888
889 let mut resource_controller = ResourceController::new(
890 Arc::new(committee_policy),
891 ResourceTracker::default(),
892 block.authenticated_owner,
893 );
894
895 for blob in published_blobs {
896 let blob_id = blob.id();
897 resource_controller
898 .policy()
899 .check_blob_size(blob.content())
900 .with_execution_context(ChainExecutionContext::Block)?;
901 chain.system.used_blobs.insert(&blob_id)?;
902 }
903
904 let mut block_execution_tracker = BlockExecutionTracker::new(
905 &mut resource_controller,
906 published_blobs
907 .iter()
908 .map(|blob| (blob.id(), blob))
909 .collect(),
910 local_time,
911 replaying_oracle_responses,
912 block,
913 phase,
914 )?;
915 if let Some(prepared) = prepared_checkpoint {
916 block_execution_tracker.set_prepared_checkpoint(prepared);
917 }
918
919 let (max_failures, never_reject_application_ids) = match &exec_policy.on_failure {
921 BundleFailurePolicy::Abort => (0, Arc::new(HashSet::new())),
922 BundleFailurePolicy::AutoRetry {
923 max_failures,
924 never_reject_application_ids,
925 } => (*max_failures, never_reject_application_ids.clone()),
926 };
927 let auto_retry = !matches!(exec_policy.on_failure, BundleFailurePolicy::Abort);
928 let mut failure_count = 0u32;
929 let mut never_reject_discarded_origins = HashSet::new();
930
931 let time_budget = exec_policy.time_budget;
932 let mut cumulative_bundle_time = Duration::ZERO;
933
934 let mut i = 0;
935 while i < block.transactions.len() {
936 let transaction = &mut block.transactions[i];
937 let is_bundle = matches!(transaction, Transaction::ReceiveMessages(_));
938 let is_stream_update = transaction.is_update_stream();
939
940 if is_bundle && time_budget.is_some_and(|budget| cumulative_bundle_time >= budget) {
942 info!(
943 ?cumulative_bundle_time,
944 ?time_budget,
945 "Time budget for bundle staging exceeded, discarding remaining bundles"
946 );
947 Self::discard_remaining_bundles(block, i, None);
948 continue;
949 }
950
951 let checkpoint = if auto_retry && (is_bundle || is_stream_update) {
952 Some((
953 chain.clone_unchecked()?,
954 block_execution_tracker.create_checkpoint(),
955 ))
956 } else {
957 None
958 };
959
960 let bundle_start = if is_bundle && time_budget.is_some() {
961 Some(Instant::now())
962 } else {
963 None
964 };
965
966 let result = block_execution_tracker
967 .execute_transaction(&*transaction, round, chain)
968 .await;
969
970 if let Some(start) = bundle_start {
971 cumulative_bundle_time += start.elapsed();
972 }
973
974 match (result, transaction, checkpoint) {
979 (Ok(()), _, _) => {
980 i += 1;
981 }
982 (
983 Err(ChainError::ExecutionError(error, _context)),
984 Transaction::ReceiveMessages(incoming_bundle),
985 Some((saved_chain, saved_tracker)),
986 ) if !error.is_transient_error() && error.is_limit_error() && i > 0 => {
987 *chain = saved_chain;
989 block_execution_tracker.restore_checkpoint(&saved_tracker);
990 failure_count += 1;
991 let maybe_sender = if failure_count > max_failures {
993 info!(
994 failure_count,
995 max_failures,
996 "Exceeded max bundle failures, discarding all remaining message \
997 bundles and stream updates"
998 );
999 Self::discard_remaining_stream_updates(block, i);
1000 None
1001 } else {
1002 info!(
1004 %error,
1005 index = i,
1006 origin = %incoming_bundle.origin,
1007 "Message bundle exceeded block limits and will be discarded for \
1008 retry in a later block"
1009 );
1010 Some(incoming_bundle.origin)
1011 };
1012 Self::discard_remaining_bundles(block, i, maybe_sender);
1013 }
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 *chain = saved_chain;
1022 block_execution_tracker.restore_checkpoint(&saved_tracker);
1023
1024 let all_messages_never_reject = !never_reject_application_ids.is_empty()
1025 && incoming_bundle.messages().all(|posted_msg| {
1026 never_reject_application_ids
1027 .contains(&posted_msg.message.application_id())
1028 });
1029 if (all_messages_never_reject || incoming_bundle.bundle.is_protected())
1030 && incoming_bundle.action != MessageAction::Reject
1031 {
1032 let origin = incoming_bundle.origin;
1033 never_reject_discarded_origins.insert(origin);
1034 warn!(
1035 %error,
1036 index = i,
1037 %origin,
1038 "Message bundle cannot be rejected (protected or never-reject); \
1039 discarding the bundle (and same-sender subsequent bundles) for retry \
1040 in a later block"
1041 );
1042 Self::discard_remaining_bundles(block, i, Some(origin));
1043 } else if incoming_bundle.action == MessageAction::Reject {
1044 return Err(ChainError::ExecutionError(error, context));
1046 } else {
1047 info!(
1050 %error,
1051 index = i,
1052 origin = %incoming_bundle.origin,
1053 "Message bundle failed to execute and will be rejected"
1054 );
1055 incoming_bundle.action = MessageAction::Reject;
1056 }
1057 }
1059 (
1060 Err(ChainError::ExecutionError(error, _context)),
1061 transaction,
1062 Some((saved_chain, saved_tracker)),
1063 ) if transaction.is_update_stream()
1064 && !error.is_transient_error()
1065 && error.is_limit_error()
1066 && i > 0 =>
1067 {
1068 *chain = saved_chain;
1070 block_execution_tracker.restore_checkpoint(&saved_tracker);
1071 failure_count += 1;
1072 if failure_count > max_failures {
1073 info!(
1074 failure_count,
1075 max_failures,
1076 "Exceeded max failures, discarding all remaining stream updates and \
1077 message bundles"
1078 );
1079 Self::discard_remaining_bundles(block, i, None);
1080 Self::discard_remaining_stream_updates(block, i);
1081 } else {
1082 info!(
1083 %error,
1084 index = i,
1085 "UpdateStream exceeded block limits, discarding for retry"
1086 );
1087 block.transactions.remove(i);
1088 }
1089 }
1091 (Err(e), _, _) => return Err(e),
1092 };
1093 }
1094
1095 ensure!(!block.transactions.is_empty(), ChainError::EmptyBlock);
1098
1099 let end_epoch = *chain.system.epoch.get();
1102 ensure!(
1103 end_epoch.0 <= start_epoch.0.saturating_add(1),
1104 ChainError::MultipleEpochAdvances {
1105 start_epoch,
1106 end_epoch,
1107 }
1108 );
1109
1110 let recipients = block_execution_tracker.recipients();
1111 let non_ack_tx_indices = block_execution_tracker.non_checkpoint_ack_tx_indices();
1112 let mut recipient_heights = Vec::new();
1113 for (recipient, height) in chain
1114 .previous_message_blocks
1115 .multi_get_pairs(recipients)
1116 .await?
1117 {
1118 if let Some(tx_indices) = non_ack_tx_indices.get(&recipient) {
1126 chain
1127 .previous_message_blocks
1128 .insert(&recipient, block.height)?;
1129 let mut cursors = chain
1135 .system
1136 .unfinalized_message_blocks
1137 .get(&recipient)
1138 .await?
1139 .unwrap_or_default();
1140 for index in tx_indices {
1141 cursors.insert(Cursor {
1142 height: block.height,
1143 index: *index,
1144 });
1145 }
1146 chain
1147 .system
1148 .unfinalized_message_blocks
1149 .insert(&recipient, cursors)?;
1150 }
1151 if let Some(height) = height {
1152 recipient_heights.push((recipient, height));
1153 }
1154 }
1155 let hashes = block_hashes
1156 .multi_get(recipient_heights.iter().map(|(_, height)| height))
1157 .await?;
1158 let mut previous_message_blocks = BTreeMap::new();
1159 for (hash, (recipient, height)) in hashes.into_iter().zip(recipient_heights) {
1160 let hash = hash.ok_or_else(|| {
1161 ChainError::CorruptedChainState("missing entry in block_hashes".into())
1162 })?;
1163 previous_message_blocks.insert(recipient, (hash, height));
1164 }
1165
1166 let streams = block_execution_tracker.event_streams();
1167 let mut stream_heights = Vec::new();
1168 for (stream, height) in chain.previous_event_blocks.multi_get_pairs(streams).await? {
1169 chain.previous_event_blocks.insert(&stream, block.height)?;
1170 if let Some(height) = height {
1171 stream_heights.push((stream, height));
1172 }
1173 }
1174 let hashes = block_hashes
1175 .multi_get(stream_heights.iter().map(|(_, height)| height))
1176 .await?;
1177 let mut previous_event_blocks = BTreeMap::new();
1178 for (hash, (stream, height)) in hashes.into_iter().zip(stream_heights) {
1179 let hash = hash.ok_or_else(|| {
1180 ChainError::CorruptedChainState("missing entry in block_hashes".into())
1181 })?;
1182 previous_event_blocks.insert(stream, (hash, height));
1183 }
1184
1185 let state_hash = {
1186 #[cfg(with_metrics)]
1187 let state_hash_latency =
1188 metrics::STATE_HASH_COMPUTATION_LATENCY.with_label_values(&[phase.as_str()]);
1189 #[cfg(with_metrics)]
1190 let _hash_latency = state_hash_latency.measure_latency_us();
1191 chain.crypto_hash_mut().await?
1192 };
1193
1194 let (messages, oracle_responses, events, blobs, operation_results, resource_tracker) =
1195 block_execution_tracker.finalize(block.transactions.len());
1196
1197 Ok((
1198 BlockExecutionOutcome {
1199 messages,
1200 previous_message_blocks,
1201 previous_event_blocks,
1202 state_hash,
1203 oracle_responses,
1204 events,
1205 blobs,
1206 operation_results,
1207 },
1208 resource_tracker,
1209 never_reject_discarded_origins,
1210 ))
1211 }
1212
1213 fn discard_remaining_stream_updates(block: &mut ProposedBlock, mut index: usize) {
1214 while index < block.transactions.len() {
1215 if block.transactions[index].is_update_stream() {
1216 block.transactions.remove(index);
1217 } else {
1218 index += 1;
1219 }
1220 }
1221 }
1222
1223 fn discard_remaining_bundles(
1224 block: &mut ProposedBlock,
1225 mut index: usize,
1226 maybe_origin: Option<ChainId>,
1227 ) {
1228 while index < block.transactions.len() {
1229 if matches!(
1230 &block.transactions[index],
1231 Transaction::ReceiveMessages(bundle)
1232 if maybe_origin.is_none_or(|origin| bundle.origin == origin)
1233 ) {
1234 block.transactions.remove(index);
1235 } else {
1236 index += 1;
1237 }
1238 }
1239 }
1240
1241 #[expect(clippy::too_many_arguments)]
1252 #[instrument(skip_all, fields(
1253 chain_id = %self.chain_id(),
1254 block_height = %block.height
1255 ))]
1256 pub async fn execute_block(
1257 &mut self,
1258 mut block: ProposedBlock,
1259 local_time: Timestamp,
1260 round: Option<u32>,
1261 published_blobs: &[Blob],
1262 replaying_oracle_responses: Option<Vec<Vec<OracleResponse>>>,
1263 policy: BundleExecutionPolicy,
1264 phase: BlockExecutionPhase,
1265 ) -> Result<
1266 (
1267 ProposedBlock,
1268 BlockExecutionOutcome,
1269 ResourceTracker,
1270 HashSet<ChainId>,
1271 ),
1272 ChainError,
1273 > {
1274 assert_eq!(
1275 block.chain_id,
1276 self.execution_state.context().extra().chain_id()
1277 );
1278
1279 self.initialize_if_needed(local_time).await?;
1280
1281 let chain_timestamp = self.execution_state.system.progress.get().timestamp;
1282 ensure!(
1283 chain_timestamp <= block.timestamp,
1284 ChainError::InvalidBlockTimestamp {
1285 parent: chain_timestamp,
1286 new: block.timestamp
1287 }
1288 );
1289 ensure!(!block.transactions.is_empty(), ChainError::EmptyBlock);
1290
1291 ensure!(
1292 block.published_blob_ids()
1293 == published_blobs
1294 .iter()
1295 .map(|blob| blob.id())
1296 .collect::<BTreeSet<_>>(),
1297 ChainError::InternalError("published_blobs mismatch".to_string())
1298 );
1299
1300 if *self.execution_state.system.closed.get() {
1301 ensure!(block.has_only_rejected_messages(), ChainError::ClosedChain);
1302 }
1303
1304 Self::check_app_permissions(
1305 self.execution_state
1306 .system
1307 .application_permissions
1308 .get()
1309 .await?,
1310 &block,
1311 )?;
1312
1313 ensure!(
1314 !block
1315 .transactions
1316 .iter()
1317 .skip(1)
1318 .any(Transaction::is_checkpoint),
1319 ChainError::CheckpointPreconditionFailed(
1320 "Checkpoint must be the first transaction in its block",
1321 )
1322 );
1323 let (origin_cursors, inbox_cursors, outbox_block_hashes) = if block.starts_with_checkpoint()
1324 {
1325 self.check_checkpoint_preconditions().await?;
1326 let origin_cursors = self.collect_inbox_cursors().await?;
1327 let inbox_cursors = self.collect_all_inbox_cursors().await?;
1328 let hashes = self.collect_unfinalized_block_hashes().await?;
1329 (origin_cursors, inbox_cursors, hashes)
1330 } else {
1331 (Vec::new(), Vec::new(), Vec::new())
1332 };
1333
1334 let (outcome, tracker, never_reject_origins) = Self::execute_block_inner(
1335 &mut self.execution_state,
1336 &self.block_hashes,
1337 &mut block,
1338 local_time,
1339 round,
1340 published_blobs,
1341 replaying_oracle_responses,
1342 policy,
1343 phase,
1344 origin_cursors,
1345 inbox_cursors,
1346 outbox_block_hashes,
1347 )
1348 .await?;
1349
1350 Ok((block, outcome, tracker, never_reject_origins))
1351 }
1352
1353 async fn collect_inbox_cursors(&self) -> Result<Vec<(ChainId, Cursor)>, ChainError> {
1361 let targets = self
1362 .execution_state
1363 .system
1364 .pending_checkpoint_ack_targets
1365 .indices()
1366 .await?;
1367 let mut cursors = Vec::with_capacity(targets.len());
1368 for origin in targets {
1369 let Some(inbox) = self.inboxes.try_load_entry(&origin).await? else {
1370 continue;
1371 };
1372 cursors.push((origin, *inbox.next_cursor_to_remove.get()));
1373 }
1374 Ok(cursors)
1375 }
1376
1377 async fn collect_all_inbox_cursors(&self) -> Result<Vec<(ChainId, Cursor)>, ChainError> {
1382 let origins = self.inboxes.indices().await?;
1383 let mut cursors = Vec::new();
1384 for origin in origins {
1385 let Some(inbox) = self.inboxes.try_load_entry(&origin).await? else {
1386 continue;
1387 };
1388 let cursor = *inbox.next_cursor_to_remove.get();
1389 if cursor != Cursor::default() {
1390 cursors.push((origin, cursor));
1391 }
1392 }
1393 Ok(cursors)
1394 }
1395
1396 pub async fn restore_outboxes_from_unfinalized(
1404 &mut self,
1405 tracked: Option<&Hashed<ChainIdSet>>,
1406 ) -> Result<(), ChainError> {
1407 let prior_recipients = self.outboxes.indices().await?;
1413 for recipient in prior_recipients {
1414 let mut outbox = self.outboxes.try_load_entry_mut(&recipient).await?;
1415 outbox.queue.clear();
1416 outbox.next_height_to_schedule.set(BlockHeight::ZERO);
1417 }
1418 let entries = self
1419 .execution_state
1420 .system
1421 .unfinalized_message_blocks
1422 .index_values()
1423 .await?;
1424 for (recipient, cursors) in entries {
1425 if cursors.is_empty() {
1426 continue;
1427 }
1428 let heights = cursors
1431 .into_iter()
1432 .map(|cursor| cursor.height)
1433 .collect::<BTreeSet<_>>();
1434 let mut outbox = self.outboxes.try_load_entry_mut(&recipient).await?;
1435 for height in &heights {
1436 outbox.queue.push_back(*height);
1437 }
1438 let max_height = *heights
1439 .last()
1440 .expect("the empty case was filtered out above");
1441 outbox
1442 .next_height_to_schedule
1443 .set(max_height.try_add_one()?);
1444 }
1445 self.rebuild_outbox_index(tracked).await?;
1447 Ok(())
1448 }
1449
1450 async fn collect_unfinalized_block_hashes(&self) -> Result<Vec<CryptoHash>, ChainError> {
1456 let heights = self.collect_unfinalized_heights().await?;
1457 let mut hashes = Vec::with_capacity(heights.len());
1458 for height in heights {
1459 let hash = self.block_hashes.get(&height).await?.ok_or_else(|| {
1460 ChainError::CorruptedChainState(format!(
1461 "missing entry in block_hashes at height {height}"
1462 ))
1463 })?;
1464 hashes.push(hash);
1465 }
1466 Ok(hashes)
1467 }
1468
1469 pub async fn collect_unfinalized_heights(&self) -> Result<BTreeSet<BlockHeight>, ChainError> {
1476 let mut heights = BTreeSet::new();
1477 let entries = self
1478 .execution_state
1479 .system
1480 .unfinalized_message_blocks
1481 .index_values()
1482 .await?;
1483 for (_, per_recipient) in entries {
1484 heights.extend(per_recipient.into_iter().map(|cursor| cursor.height));
1485 }
1486 Ok(heights)
1487 }
1488
1489 #[instrument(skip_all, fields(
1493 chain_id = %self.chain_id(),
1494 block_height = %block.inner().inner().header.height
1495 ))]
1496 pub async fn apply_confirmed_block(
1497 &mut self,
1498 block: &ConfirmedBlock,
1499 local_time: Timestamp,
1500 tracked: Option<&ChainIdSet>,
1501 ) -> Result<BTreeSet<StreamId>, ChainError> {
1502 let hash = block.inner().hash();
1503 let block = block.inner().inner();
1504 if block.header.height == BlockHeight::ZERO {
1505 self.chain_initialized_at.set(local_time);
1506 }
1507 let updated_streams = self.process_emitted_events(block).await?;
1508 self.process_outgoing_messages(block, tracked).await?;
1509
1510 self.reset_chain_manager(block.header.height.try_add_one()?, local_time)
1512 .await?;
1513
1514 let tip = self.tip_state.get_mut();
1516 tip.block_hash = Some(hash);
1517 tip.next_block_height.try_add_assign_one()?;
1518 self.insert_block_hash(block.header.height, hash)?;
1519 if block.body.starts_with_checkpoint() {
1520 self.latest_checkpoint_height.set(Some(block.header.height));
1521 }
1522 Ok(updated_streams)
1523 }
1524
1525 #[instrument(skip_all, fields(
1528 chain_id = %self.chain_id(),
1529 block_height = %block.inner().inner().header.height
1530 ))]
1531 pub async fn preprocess_block(
1532 &mut self,
1533 block: &ConfirmedBlock,
1534 tracked: Option<&ChainIdSet>,
1535 ) -> Result<BTreeSet<StreamId>, ChainError> {
1536 let hash = block.inner().hash();
1537 let block = block.inner().inner();
1538 let height = block.header.height;
1539 if height < self.tip_state.get().next_block_height {
1540 return Ok(BTreeSet::new());
1541 }
1542 self.process_outgoing_messages(block, tracked).await?;
1543 let updated_streams = self.process_emitted_events(block).await?;
1544 self.insert_block_hash(height, hash)?;
1545 Ok(updated_streams)
1546 }
1547
1548 #[instrument(skip_all, fields(
1550 block_height = %block.height,
1551 num_transactions = %block.transactions.len()
1552 ))]
1553 fn check_app_permissions(
1554 app_permissions: &ApplicationPermissions,
1555 block: &ProposedBlock,
1556 ) -> Result<(), ChainError> {
1557 let mut mandatory = app_permissions
1558 .mandatory_applications
1559 .iter()
1560 .copied()
1561 .collect::<HashSet<ApplicationId>>();
1562 for transaction in &block.transactions {
1563 match transaction {
1564 Transaction::ExecuteOperation(operation)
1565 if operation.is_exempt_from_permissions() =>
1566 {
1567 mandatory.clear()
1568 }
1569 Transaction::ExecuteOperation(operation) => {
1570 ensure!(
1571 app_permissions.can_execute_operations(&operation.application_id()),
1572 ChainError::AuthorizedApplications(
1573 app_permissions.execute_operations.clone().unwrap()
1574 )
1575 );
1576 if let Operation::User { application_id, .. } = operation {
1577 mandatory.remove(application_id);
1578 }
1579 }
1580 Transaction::ReceiveMessages(incoming_bundle)
1581 if incoming_bundle.action == MessageAction::Accept =>
1582 {
1583 for pending in incoming_bundle.messages() {
1584 if let Message::User { application_id, .. } = &pending.message {
1585 mandatory.remove(application_id);
1586 }
1587 }
1588 }
1589 Transaction::ReceiveMessages(_) => {}
1590 }
1591 }
1592 ensure!(
1593 mandatory.is_empty(),
1594 ChainError::MissingMandatoryApplications(mandatory.into_iter().collect())
1595 );
1596 Ok(())
1597 }
1598
1599 async fn check_checkpoint_preconditions(&self) -> Result<(), ChainError> {
1607 let mut had_system_event_tracker = false;
1608 self.next_expected_events
1609 .for_each_index_while(|stream_id| {
1610 if matches!(stream_id.application_id, GenericApplicationId::System) {
1611 had_system_event_tracker = true;
1612 Ok(false)
1613 } else {
1614 Ok(true)
1615 }
1616 })
1617 .await?;
1618 ensure!(
1619 !had_system_event_tracker,
1620 ChainError::CheckpointPreconditionFailed("chain has consumed system events")
1621 );
1622
1623 Ok(())
1624 }
1625
1626 #[instrument(skip_all, fields(
1629 chain_id = %self.chain_id(),
1630 next_block_height = %self.tip_state.get().next_block_height,
1631 ))]
1632 pub async fn block_hashes_for_heights(
1633 &self,
1634 heights: impl IntoIterator<Item = BlockHeight>,
1635 ) -> Result<Vec<CryptoHash>, ChainError> {
1636 let heights = heights.into_iter().collect::<Vec<_>>();
1637 Ok(self
1638 .block_hashes
1639 .multi_get(&heights)
1640 .await?
1641 .into_iter()
1642 .flatten()
1643 .collect())
1644 }
1645
1646 async fn reset_chain_manager(
1648 &mut self,
1649 next_height: BlockHeight,
1650 local_time: Timestamp,
1651 ) -> Result<(), ChainError> {
1652 let maybe_committee = self
1653 .execution_state
1654 .system
1655 .current_committee()
1656 .await
1657 .with_execution_context(ChainExecutionContext::Block)?;
1658 let ownership = self.execution_state.system.ownership.get().await?.clone();
1659 let fallback_owners = maybe_committee
1660 .iter()
1661 .flat_map(|(_, committee)| committee.account_keys_and_weights());
1662 self.pending_validated_blobs.clear();
1663 self.pending_proposed_blobs.clear();
1664 self.manager
1665 .reset(ownership, next_height, local_time, fallback_owners)
1666 }
1667
1668 #[instrument(skip_all, fields(
1672 chain_id = %self.chain_id(),
1673 block_height = %block.header.height
1674 ))]
1675 async fn process_outgoing_messages(
1676 &mut self,
1677 block: &Block,
1678 tracked: Option<&ChainIdSet>,
1679 ) -> Result<Vec<ChainId>, ChainError> {
1680 let recipients = block.recipients();
1683 let block_height = block.header.height;
1684 let next_height = self.tip_state.get().next_block_height;
1685
1686 let targets = recipients.into_iter().collect::<Vec<_>>();
1689 let outboxes = self.outboxes.try_load_entries_mut(&targets).await?;
1690 let mut scheduled_tracked = Vec::new();
1691 for (mut outbox, target) in outboxes.into_iter().zip(&targets) {
1692 if block_height > next_height {
1693 if *outbox.next_height_to_schedule.get() > block_height {
1696 continue; }
1698 let maybe_prev_hash = match outbox.next_height_to_schedule.get().try_sub_one().ok()
1699 {
1700 Some(height) => {
1701 Some(self.block_hashes.get(&height).await?.ok_or_else(|| {
1702 ChainError::CorruptedChainState("missing entry in block_hashes".into())
1703 })?)
1704 }
1705 None => None, };
1707 match (
1709 maybe_prev_hash,
1710 block.body.previous_message_blocks.get(target),
1711 ) {
1712 (None, None) => {
1713 }
1716 (Some(_), None) => {
1717 }
1725 (None, Some((_, prev_msg_block_height))) => {
1726 if *prev_msg_block_height >= next_height {
1731 continue;
1732 }
1733 }
1734 (Some(ref prev_hash), Some((prev_msg_block_hash, _))) => {
1735 if prev_hash != prev_msg_block_hash {
1741 continue;
1742 }
1743 }
1744 }
1745 }
1746 if outbox.schedule_message(block_height)?
1747 && tracked.is_none_or(|set| set.contains(target))
1748 {
1749 scheduled_tracked.push(*target);
1750 }
1751 #[cfg(with_metrics)]
1752 crate::outbox::metrics::OUTBOX_SIZE
1753 .with_label_values(&[])
1754 .observe(outbox.queue.count() as f64);
1755 }
1756
1757 if !scheduled_tracked.is_empty() {
1758 let scheduled_count =
1760 u32::try_from(scheduled_tracked.len()).map_err(|_| ArithmeticError::Overflow)?;
1761 *self
1762 .outbox_counters
1763 .get_mut()
1764 .entry(block_height)
1765 .or_default() += scheduled_count;
1766 let nonempty_outboxes = self.nonempty_outboxes.get_mut();
1767 for target in &scheduled_tracked {
1768 nonempty_outboxes.insert(*target);
1769 }
1770 }
1771
1772 #[cfg(with_metrics)]
1773 metrics::NUM_OUTBOXES
1774 .with_label_values(&[])
1775 .observe(self.nonempty_outboxes.get().len() as f64);
1776 #[cfg(with_metrics)]
1777 metrics::OUTBOX_COUNTERS_SIZE
1778 .with_label_values(&[])
1779 .observe(self.outbox_counters.get().len() as f64);
1780 Ok(targets)
1781 }
1782
1783 #[instrument(skip_all, fields(
1788 chain_id = %self.chain_id(),
1789 block_height = %block.header.height
1790 ))]
1791 async fn process_emitted_events(
1792 &mut self,
1793 block: &Block,
1794 ) -> Result<BTreeSet<StreamId>, ChainError> {
1795 let mut emitted_ranges = BTreeMap::<StreamId, (u32, u32)>::new();
1798 for event in block.body.events.iter().flatten() {
1799 emitted_ranges
1800 .entry(event.stream_id.clone())
1801 .and_modify(|(lo, hi)| {
1802 *lo = (*lo).min(event.index);
1803 *hi = (*hi).max(event.index);
1804 })
1805 .or_insert((event.index, event.index));
1806 }
1807 let mut stream_ids = Vec::new();
1808 let mut ranges = Vec::new();
1809 for (stream_id, range) in emitted_ranges {
1810 stream_ids.push(stream_id);
1811 ranges.push(range);
1812 }
1813
1814 let mut updated_streams = BTreeSet::new();
1815 for ((stream_id, counts), (lo, hi)) in self
1816 .next_expected_events
1817 .multi_get_pairs(stream_ids)
1818 .await?
1819 .into_iter()
1820 .zip(ranges)
1821 {
1822 let mut counts = counts.unwrap_or_default();
1823 let next = hi.saturating_add(1);
1824 if block.body.previous_event_blocks.contains_key(&stream_id) {
1825 if lo != counts.next_index {
1829 continue;
1830 }
1831 counts.next_index = next;
1832 } else if lo >= counts.first_index {
1833 counts.first_index = lo;
1838 counts.next_index = counts.next_index.max(next);
1839 } else {
1840 continue;
1842 }
1843 updated_streams.insert(stream_id.clone());
1844 self.next_expected_events.insert(&stream_id, counts)?;
1845 }
1846 Ok(updated_streams)
1847 }
1848}
1849
1850#[test]
1851fn empty_block_size() {
1852 let size = bcs::serialized_size(&crate::block::Block::new(
1853 crate::test::make_first_block(
1854 linera_execution::test_utils::dummy_chain_description(0).id(),
1855 ),
1856 crate::data_types::BlockExecutionOutcome::default(),
1857 ))
1858 .unwrap();
1859 assert_eq!(size, EMPTY_BLOCK_SIZE);
1860}