Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 53 additions & 13 deletions linera-chain/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,56 @@ impl BlockExecutionPhase {
}
}

/// What a call to [`ChainStateView::execute_block`] is doing, carrying exactly the inputs that
/// are legal for that phase.
///
/// Bundling the phase together with the replayed oracle responses and the bundle-execution
/// policy makes the illegal combinations unrepresentable: only [`StageProposal`] may choose a
/// policy (and thus `AutoRetry`), and only [`HandleConfirmed`] carries oracle responses to
/// replay — so "replay oracle responses while auto-retrying" cannot be constructed.
///
/// [`StageProposal`]: BlockExecution::StageProposal
/// [`HandleConfirmed`]: BlockExecution::HandleConfirmed
pub enum BlockExecution {
/// A proposer staging (building) its own block. The bundle-failure policy is caller-chosen
/// (and may be `AutoRetry`); oracle responses are computed fresh, never replayed.
StageProposal {
/// How to handle failing bundles while building the proposal.
policy: BundleExecutionPolicy,
},
/// A validator validating a received block proposal. Bundles must abort on failure (the
/// proposal is fixed) and oracle responses are computed fresh.
HandleProposal,
/// A validator executing a confirmed certificate before committing it. Bundles must abort
/// on failure and the certificate's recorded oracle responses are replayed for determinism.
HandleConfirmed {
/// The oracle responses recorded in the certificate, replayed to reproduce the outcome.
oracle_responses: Vec<Vec<OracleResponse>>,
},
}

impl BlockExecution {
/// The protocol phase this execution represents, used to label execution metrics and spans.
pub fn phase(&self) -> BlockExecutionPhase {
match self {
BlockExecution::StageProposal { .. } => BlockExecutionPhase::StageProposal,
BlockExecution::HandleProposal => BlockExecutionPhase::HandleProposal,
BlockExecution::HandleConfirmed { .. } => BlockExecutionPhase::HandleConfirmed,
}
}

/// Splits into the oracle responses to replay (if any) and the bundle-execution policy.
fn into_oracle_and_policy(self) -> (Option<Vec<Vec<OracleResponse>>>, BundleExecutionPolicy) {
match self {
BlockExecution::StageProposal { policy } => (None, policy),
BlockExecution::HandleProposal => (None, BundleExecutionPolicy::committed()),
BlockExecution::HandleConfirmed { oracle_responses } => {
(Some(oracle_responses), BundleExecutionPolicy::committed())
}
}
}
}

#[cfg(with_metrics)]
pub(crate) mod metrics {
use std::sync::LazyLock;
Expand Down Expand Up @@ -830,15 +880,6 @@ where
checkpoint_inbox_cursors: Vec<(ChainId, Cursor)>,
checkpoint_outbox_block_hashes: Vec<CryptoHash>,
) -> Result<(BlockExecutionOutcome, ResourceTracker, HashSet<ChainId>), ChainError> {
// AutoRetry is incompatible with replaying oracle responses because discarding or
// rejecting bundles would change which transactions execute.
if !matches!(&exec_policy.on_failure, BundleFailurePolicy::Abort) {
assert!(
replaying_oracle_responses.is_none(),
"Cannot use AutoRetry policy when replaying oracle responses"
);
}

#[cfg(with_metrics)]
let block_execution_latency =
metrics::BLOCK_EXECUTION_LATENCY.with_label_values(&[phase.as_str()]);
Expand Down Expand Up @@ -1235,7 +1276,6 @@ where
/// - After `max_failures` failed bundles, all remaining message bundles are discarded.
///
/// The block may be modified to reflect the actual executed transactions.
#[expect(clippy::too_many_arguments)]
#[instrument(skip_all, fields(
chain_id = %self.chain_id(),
block_height = %block.height
Expand All @@ -1246,9 +1286,7 @@ where
local_time: Timestamp,
round: Option<u32>,
published_blobs: &[Blob],
replaying_oracle_responses: Option<Vec<Vec<OracleResponse>>>,
policy: BundleExecutionPolicy,
phase: BlockExecutionPhase,
execution: BlockExecution,
) -> Result<
(
ProposedBlock,
Expand Down Expand Up @@ -1318,6 +1356,8 @@ where
(Vec::new(), Vec::new(), Vec::new())
};

let phase = execution.phase();
let (replaying_oracle_responses, policy) = execution.into_oracle_and_policy();
let (outcome, tracker, never_reject_origins) = Self::execute_block_inner(
&mut self.execution_state,
&self.block_hashes,
Expand Down
4 changes: 3 additions & 1 deletion linera-chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ mod pending_blobs;
#[cfg(with_testing)]
pub mod test;

pub use chain::{BlockExecutionPhase, ChainIdSet, ChainStateView, ChainTipState, StreamCounts};
pub use chain::{
BlockExecution, BlockExecutionPhase, ChainIdSet, ChainStateView, ChainTipState, StreamCounts,
};
use data_types::{MessageBundle, PostedMessage};
use linera_base::{
bcs,
Expand Down
8 changes: 4 additions & 4 deletions linera-chain/src/unit_tests/chain_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::{
PostedMessage, ProposedBlock,
},
test::{make_child_block, make_first_block, BlockTestExt, HttpServer},
BlockExecutionPhase, ChainError, ChainExecutionContext, ChainStateView,
BlockExecution, ChainError, ChainExecutionContext, ChainStateView,
};

impl ChainStateView<MemoryContext<TestExecutionRuntimeContext>> {
Expand Down Expand Up @@ -73,9 +73,9 @@ impl ChainStateView<MemoryContext<TestExecutionRuntimeContext>> {
local_time,
None,
published_blobs,
None,
BundleExecutionPolicy::committed(),
BlockExecutionPhase::StageProposal,
BlockExecution::StageProposal {
policy: BundleExecutionPolicy::committed(),
},
)
.await?;
Ok((block, outcome, tracker))
Expand Down
36 changes: 13 additions & 23 deletions linera-core/src/chain_worker/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ use linera_chain::{
Block, ConfirmedBlock, ConfirmedBlockCertificate, TimeoutCertificate,
ValidatedBlockCertificate,
},
BlockExecutionPhase, ChainError, ChainExecutionContext, ChainIdSet, ChainStateView,
ChainTipState, ExecutionResultExt as _, StreamCounts,
BlockExecution, ChainError, ChainExecutionContext, ChainIdSet, ChainStateView, ChainTipState,
ExecutionResultExt as _, StreamCounts,
};
use linera_execution::{
system::{EpochEventData, EventSubscriptions, EPOCH_STREAM_NAME},
Expand Down Expand Up @@ -1306,16 +1306,15 @@ where
certificate.into_value()
} else {
let (proposed_block, outcome) = certificate.into_value().into_block().into_proposal();
let oracle_responses = Some(outcome.oracle_responses.clone());
let (proposed_block, verified, _resource_tracker, _) = chain
.execute_block(
proposed_block,
local_time,
None,
&published_blobs,
oracle_responses,
BundleExecutionPolicy::committed(),
BlockExecutionPhase::HandleConfirmed,
BlockExecution::HandleConfirmed {
oracle_responses: outcome.oracle_responses.clone(),
},
)
.await?;
// We should always agree on the messages and state hash.
Expand Down Expand Up @@ -2323,8 +2322,7 @@ where
local_time,
round,
published_blobs,
policy,
BlockExecutionPhase::StageProposal,
BlockExecution::StageProposal { policy },
))
.await?;

Expand Down Expand Up @@ -2531,8 +2529,7 @@ where
local_time,
round.multi_leader(),
&published_blobs,
BundleExecutionPolicy::committed(),
BlockExecutionPhase::HandleProposal,
BlockExecution::HandleProposal,
))
.await?;
executed_block
Expand Down Expand Up @@ -2696,20 +2693,13 @@ where
local_time: Timestamp,
round: Option<u32>,
published_blobs: &[Blob],
policy: BundleExecutionPolicy,
phase: BlockExecutionPhase,
execution: BlockExecution,
) -> Result<(Block, ResourceTracker, HashSet<ChainId>), WorkerError> {
let (proposed_block, outcome, resource_tracker, never_reject_origins) =
Box::pin(self.chain.execute_block(
block,
local_time,
round,
published_blobs,
None,
policy,
phase,
))
.await?;
let (proposed_block, outcome, resource_tracker, never_reject_origins) = Box::pin(
self.chain
.execute_block(block, local_time, round, published_blobs, execution),
)
.await?;
let executed_block = Block::new(proposed_block, outcome);
let block_hash = executed_block.hash();
if let Some(cache) = &self.execution_state_cache {
Expand Down
Loading