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, 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#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr)]
75#[strum(serialize_all = "snake_case")]
76pub enum BlockExecutionPhase {
77 StageProposal,
79 HandleProposal,
81 HandleConfirmed,
84}
85
86pub enum BlockExecution {
97 StageProposal {
100 policy: BundleExecutionPolicy,
102 },
103 HandleProposal,
106 HandleConfirmed {
109 oracle_responses: Vec<Vec<OracleResponse>>,
111 },
112}
113
114impl BlockExecution {
115 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 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 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
279pub(crate) const EMPTY_BLOCK_SIZE: usize = 94;
281
282#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
284#[derive(Debug, Clone, Serialize, Deserialize, Allocative)]
285pub struct TimestampedBundleInInbox {
286 pub entry: BundleInInbox,
288 pub seen: Timestamp,
290}
291
292#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
294#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Allocative)]
295pub struct BundleInInbox {
296 pub origin: ChainId,
298 pub cursor: Cursor,
300}
301
302const TIMESTAMPBUNDLE_BUCKET_SIZE: usize = 100;
305
306#[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#[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 pub execution_state: ExecutionStateView<C>,
338 pub execution_state_hash: RegisterView<C, Option<CryptoHash>>,
340
341 pub tip_state: RegisterView<C, ChainTipState>,
343
344 pub manager: ChainManager<C>,
346 pub pending_validated_blobs: PendingBlobsView<C>,
349 pub pending_proposed_blobs: ReentrantCollectionView<C, AccountOwner, PendingBlobsView<C>>,
351
352 pub confirmed_log: LogView<C, CryptoHash>,
355 pub received_log: LogView<C, ChainAndHeight>,
357 pub received_certificate_trackers: RegisterView<C, HashMap<ValidatorPublicKey, u64>>,
359
360 pub inboxes: ReentrantCollectionView<C, ChainId, InboxStateView<C>>,
362 pub unskippable_bundles:
364 BucketQueueView<C, TimestampedBundleInInbox, TIMESTAMPBUNDLE_BUCKET_SIZE>,
365 pub removed_unskippable_bundles: SetView<C, BundleInInbox>,
367 pub previous_message_blocks: MapView<C, ChainId, BlockHeight>,
369 pub previous_event_blocks: MapView<C, StreamId, BlockHeight>,
371 pub outboxes: ReentrantCollectionView<C, ChainId, OutboxStateView<C>>,
373 pub outbox_counters: RegisterView<C, NonCanonicalBTreeMap<BlockHeight, u32>>,
376 pub nonempty_outboxes: RegisterView<C, NonCanonicalBTreeSet<ChainId>>,
378
379 pub preprocessed_blocks: MapView<C, BlockHeight, CryptoHash>,
381
382 pub next_expected_events: MapView<C, StreamId, u32>,
385
386 pub nonempty_inboxes: RegisterView<C, Option<NonCanonicalBTreeSet<ChainId>>>,
390
391 pub block_zero_executed_at: RegisterView<C, Timestamp>,
395
396 pub outbox_index_tracked_hash: RegisterView<C, Option<CryptoHash>>,
403}
404
405#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
407#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
408pub struct ChainTipState {
409 pub block_hash: Option<CryptoHash>,
411 pub next_block_height: BlockHeight,
413 pub num_incoming_bundles: u32,
415 pub num_operations: u32,
417 pub num_outgoing_messages: u32,
419}
420
421impl ChainTipState {
422 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 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 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 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 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 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 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 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 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 *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 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 pub async fn is_active(&self) -> Result<bool, ChainError> {
618 Ok(self.execution_state.system.is_active().await?)
619 }
620
621 pub async fn initialize_if_needed(&mut self, local_time: Timestamp) -> Result<(), ChainError> {
623 if self
625 .execution_state
626 .system
627 .initialize_chain(self.chain_id())
628 .await
629 .with_execution_context(ChainExecutionContext::Block)?
630 {
631 return Ok(());
633 }
634 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 pub async fn next_height_to_preprocess(&self) -> Result<BlockHeight, ChainError> {
645 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 #[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 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 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 if add_to_received_log {
715 self.received_log.push(chain_and_height);
716 }
717 Ok(())
718 }
719
720 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 if tracker > *t {
733 *t = tracker;
734 }
735 })
736 .or_insert(tracker);
737 }
738 }
739
740 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 pub async fn ownership(&self) -> Result<&ChainOwnership, ChainError> {
751 Ok(self.execution_state.system.ownership.get().await?)
752 }
753
754 #[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 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 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 pub fn nonempty_outbox_chain_ids(&self) -> Vec<ChainId> {
826 self.nonempty_outboxes.get().iter().copied().collect()
827 }
828
829 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 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 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 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 #[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 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 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 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 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 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 if let Some(start) = bundle_start {
1002 cumulative_bundle_time += start.elapsed();
1003 }
1004
1005 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 *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 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 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 } 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 } else if incoming_bundle.action == MessageAction::Reject {
1072 return Err(ChainError::ExecutionError(error, context));
1074 } else {
1075 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 }
1086 }
1087
1088 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 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 #[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 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 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 #[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 self.reset_chain_manager(block.header.height.try_add_one()?, local_time)
1354 .await?;
1355
1356 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 #[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 #[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 #[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 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 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 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 #[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 let recipients = block.recipients();
1508 let block_height = block.header.height;
1509 let next_height = self.tip_state.get().next_block_height;
1510
1511 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 if *outbox.next_height_to_schedule.get() > block_height {
1521 continue; }
1523 let maybe_prev_hash = match outbox.next_height_to_schedule.get().try_sub_one().ok()
1524 {
1525 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 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, };
1545 match (
1547 maybe_prev_hash,
1548 block.body.previous_message_blocks.get(target),
1549 ) {
1550 (None, None) => {
1551 }
1554 (Some(_), None) => {
1555 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 if *prev_msg_block_height >= next_height {
1569 continue;
1570 }
1571 }
1572 (Some(ref prev_hash), Some((prev_msg_block_hash, _))) => {
1573 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 *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}