Skip to content

Commit 2716717

Browse files
committed
fix(chain-worker): validate canonical ASM manifests before execution
1 parent 933b9f4 commit 2716717

15 files changed

Lines changed: 457 additions & 29 deletions

File tree

‎bin/strata/src/fcm.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ impl ChainController for StrataFcmContext {
100100
warn!(%block, %err, "deferring block after local worker failure");
101101
Ok(BlockExecutionOutcome::Deferred(ExecutionDeferral::Storage))
102102
}
103+
Err(WorkerError::ManifestPending { height, reason }) => {
104+
debug!(%block, height, ?reason, "deferring unvalidated ASM manifest");
105+
Ok(BlockExecutionOutcome::Deferred(
106+
ExecutionDeferral::Dependency,
107+
))
108+
}
109+
Err(WorkerError::ManifestStorage(err)) if is_retryable_database_error(&err) => {
110+
warn!(%block, %err, "canonical manifest storage unavailable");
111+
Ok(BlockExecutionOutcome::Deferred(ExecutionDeferral::Storage))
112+
}
113+
Err(err @ WorkerError::ManifestStorage(_)) => Err(err).with_context(|| {
114+
format!("cannot validate block {block}: canonical manifest storage failed; repair required")
115+
}),
103116
Err(WorkerError::StfExecution(err)) => {
104117
warn!(%block, %err, "rejecting invalid block execution");
105118
Ok(BlockExecutionOutcome::Rejected)

‎crates/chain-worker/src/context.rs‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ use strata_asm_checkpoint_types::CheckpointPayload;
1414
use strata_asm_common::AsmManifest;
1515
use strata_checkpoint_types::EpochSummary;
1616
use strata_db_types::{
17+
DbResult,
1718
errors::DbError,
1819
ol_state_index::{AccountUpdateMeta, AccountUpdateRecord, InboxMessageRecord, IndexingWrites},
1920
};
20-
use strata_identifiers::{AccountId, Hash, OLBlockCommitment, OLBlockId};
21+
use strata_identifiers::{AccountId, Hash, L1Height, OLBlockCommitment, OLBlockId};
2122
use strata_msg_fmt::{Msg, MsgRef};
2223
use strata_node_context::NodeContext;
2324
use strata_ol_chain_types_v1::{
@@ -84,6 +85,7 @@ pub struct ChainWorkerContextImpl {
8485

8586
/// Runtime parameters for OL STF execution.
8687
runtime_params: OLRuntimeParams,
88+
l1_reorg_safe_depth: u32,
8789

8890
/// Runtime handle
8991
handle: Handle,
@@ -104,6 +106,7 @@ impl ChainWorkerContextImpl {
104106
epoch_summary_tx,
105107
ol_params: nodectx.ol_params().clone(),
106108
runtime_params: nodectx.ol_params().runtime_params(),
109+
l1_reorg_safe_depth: nodectx.config().btcio.l1_reorg_safe_depth,
107110
handle: nodectx.executor().handle().clone(),
108111
}
109112
}
@@ -122,6 +125,26 @@ impl ChainWorkerContextImpl {
122125
}
123126

124127
impl ChainWorkerContext for ChainWorkerContextImpl {
128+
fn l1_reorg_safe_depth(&self) -> u32 {
129+
self.l1_reorg_safe_depth
130+
}
131+
132+
fn canonical_l1_tip_height(&self) -> DbResult<Option<L1Height>> {
133+
self.l1_block_mgr
134+
.get_canonical_chain_tip()
135+
.map(|tip| tip.map(|(height, _)| height))
136+
}
137+
138+
fn canonical_manifest(&self, height: L1Height) -> DbResult<Option<AsmManifest>> {
139+
let Some(id) = self
140+
.l1_block_mgr
141+
.get_canonical_blockid_at_height_uncached(height)?
142+
else {
143+
return Ok(None);
144+
};
145+
self.l1_block_mgr.get_block_manifest(&id)
146+
}
147+
125148
fn runtime_params(&self) -> OLRuntimeParams {
126149
self.runtime_params
127150
}

‎crates/chain-worker/src/errors.rs‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use strata_acct_types::AccountSerial;
44
use strata_codec::CodecError;
55
use strata_db_types::errors::DbError;
6-
use strata_identifiers::{AccountId, Buf32, Epoch, OLBlockCommitment, OLBlockId};
6+
use strata_identifiers::{AccountId, Buf32, Epoch, L1Height, OLBlockCommitment, OLBlockId};
77
use strata_ol_state_types::StateError;
88
use strata_primitives::epoch::EpochCommitment;
99
use strata_snark_acct_types::Seqno;
@@ -12,9 +12,28 @@ use thiserror::Error;
1212
/// Return type for worker messages.
1313
pub type WorkerResult<T> = Result<T, WorkerError>;
1414

15+
/// Explains why canonical ASM provenance cannot yet be established.
16+
#[derive(Clone, Copy, Debug)]
17+
pub enum ManifestPendingReason {
18+
MissingTip,
19+
NotBuried,
20+
MissingManifest,
21+
}
22+
1523
/// Errors that can occur during chain worker operations.
1624
#[derive(Debug, Error)]
1725
pub enum WorkerError {
26+
/// Canonical L1/ASM data does not yet validate a carried manifest.
27+
#[error("ASM manifest at height {height} is pending: {reason:?}")]
28+
ManifestPending {
29+
height: L1Height,
30+
reason: ManifestPendingReason,
31+
},
32+
33+
/// A canonical validation read failed; this is not an invalid block verdict.
34+
#[error("failed to read canonical ASM provenance: {0}")]
35+
ManifestStorage(#[source] DbError),
36+
1837
/// Block not found in database.
1938
#[error("missing OL block {0}")]
2039
MissingOLBlock(OLBlockId),

‎crates/chain-worker/src/lib.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ mod handle;
2020
mod message;
2121
mod mmr_prefill;
2222
mod output;
23+
mod provenance;
2324
mod service;
2425
mod state;
2526
mod traits;
@@ -28,7 +29,7 @@ mod traits;
2829
mod tests;
2930

3031
pub use context::ChainWorkerContextImpl;
31-
pub use errors::{WorkerError, WorkerResult};
32+
pub use errors::{ManifestPendingReason, WorkerError, WorkerResult};
3233
pub use handle::ChainWorkerHandle;
3334
pub use message::ChainWorkerMessage;
3435
pub use mmr_prefill::{prefill_l1_block_refs_mmr, prefill_l1_block_refs_mmr_blocking};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//! Runtime validation of complete ASM manifests against stable canonical L1 history.
2+
3+
use strata_asm_common::AsmManifest;
4+
use strata_identifiers::L1Height;
5+
use strata_ol_stf_v1::{ExecError, validate_manifest_heights};
6+
7+
use crate::{ChainWorkerContext, ManifestPendingReason, WorkerError, WorkerResult};
8+
9+
/// Validates the entire range before the STF can buffer even its first log.
10+
///
11+
/// Reorgs beyond the configured L1 depth are unsupported. Restricting reads to the
12+
/// buried prefix makes the range coherent without trusting cached height mappings.
13+
pub(crate) fn validate_manifests(
14+
ctx: &impl ChainWorkerContext,
15+
last_height: L1Height,
16+
manifests: &[AsmManifest],
17+
) -> WorkerResult<()> {
18+
// Validate every height first; a later malformed height is not a missing dependency.
19+
validate_manifest_heights(last_height, manifests)?;
20+
21+
let Some(first) = manifests.first() else {
22+
return Ok(());
23+
};
24+
25+
let tip = ctx
26+
.canonical_l1_tip_height()
27+
.map_err(WorkerError::ManifestStorage)?
28+
.ok_or(WorkerError::ManifestPending {
29+
height: first.height(),
30+
reason: ManifestPendingReason::MissingTip,
31+
})?;
32+
let buried_tip = tip.checked_sub(ctx.l1_reorg_safe_depth().saturating_sub(1));
33+
34+
for manifest in manifests {
35+
let height = manifest.height();
36+
if buried_tip.is_none_or(|tip| height > tip) {
37+
return Err(WorkerError::ManifestPending {
38+
height,
39+
reason: ManifestPendingReason::NotBuried,
40+
});
41+
}
42+
43+
let canonical = ctx
44+
.canonical_manifest(height)
45+
.map_err(WorkerError::ManifestStorage)?
46+
.ok_or(WorkerError::ManifestPending {
47+
height,
48+
reason: ManifestPendingReason::MissingManifest,
49+
})?;
50+
51+
if canonical.compute_hash() != manifest.compute_hash() {
52+
return Err(ExecError::AsmManifestContentMismatch { height }.into());
53+
}
54+
}
55+
56+
Ok(())
57+
}

‎crates/chain-worker/src/state.rs‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ use strata_ol_state_types::{
3535
IAccountState, ISnarkAccountState, IStateAccessor, StateError, StateResult,
3636
};
3737
use strata_ol_state_types_v1::{IStateBatchApplicable, OLStateV1, WriteBatch};
38-
use strata_ol_stf_v1::{BlockInfo, EpochInfo, apply_da_epoch, verify_block};
38+
use strata_ol_stf_v1::{
39+
BlockInfo, EpochInfo, apply_da_epoch, verify_block, verify_block_structure,
40+
verify_header_continuity,
41+
};
3942
use strata_primitives::{epoch::EpochCommitment, l1::L1BlockCommitment};
4043
use strata_service::ServiceState;
4144
use strata_snark_acct_types::Seqno;
@@ -45,6 +48,7 @@ use crate::{
4548
ChainWorkerContextImpl,
4649
errors::{WorkerError, WorkerResult},
4750
output::OLBlockExecutionOutput,
51+
provenance::validate_manifests,
4852
traits::ChainWorkerContext,
4953
};
5054

@@ -324,10 +328,37 @@ fn fetch_block_with_parent(
324328
Ok((block, parent_header, parent_commitment))
325329
}
326330

331+
/// Validates all block inputs without executing or persisting state.
332+
fn validate_block_inputs(
333+
ctx: &impl ChainWorkerContext,
334+
block: &OLBlockV1,
335+
parent_header: Option<&OLBlockHeaderV1>,
336+
parent_state: &impl IStateAccessor,
337+
) -> WorkerResult<()> {
338+
verify_header_continuity(block.header(), parent_header)?;
339+
verify_block_structure(block.header(), block.body())?;
340+
if let Some(container) = block.body().manifests() {
341+
validate_manifests(ctx, parent_state.last_l1_height(), container.manifests())?;
342+
}
343+
Ok(())
344+
}
345+
346+
/// Loads the parent state used by input validation and STF execution.
347+
fn fetch_parent_state(
348+
ctx: &impl ChainWorkerContext,
349+
parent_commitment: OLBlockCommitment,
350+
) -> WorkerResult<MemoryStateBaseLayer> {
351+
let parent_state = ctx
352+
.fetch_ol_state(parent_commitment)?
353+
.ok_or(WorkerError::MissingPreState(parent_commitment))?;
354+
Ok(MemoryStateBaseLayer::new(parent_state))
355+
}
356+
327357
/// Executes the STF on a block and returns the execution output.
328358
///
329359
/// This fetches parent state, builds the state stack, runs verification,
330360
/// and extracts the resulting write batch and indexer writes.
361+
/// Callers classify failures; pending canonical manifests are expected during L1 catch-up.
331362
#[instrument(
332363
skip_all,
333364
fields(
@@ -336,7 +367,6 @@ fn fetch_block_with_parent(
336367
is_terminal = block.header().is_terminal(),
337368
%parent_commitment,
338369
),
339-
err,
340370
)]
341371
fn execute_stf(
342372
ctx: &impl ChainWorkerContext,
@@ -345,11 +375,8 @@ fn execute_stf(
345375
parent_header: Option<&OLBlockHeaderV1>,
346376
parent_commitment: OLBlockCommitment,
347377
) -> WorkerResult<(OLBlockExecutionOutput, OLStateV1)> {
348-
// Fetch parent state and wrap in MemoryStateBaseLayer for IStateAccessor
349-
let parent_state_raw = ctx
350-
.fetch_ol_state(parent_commitment)?
351-
.ok_or(WorkerError::MissingPreState(parent_commitment))?;
352-
let parent_state = MemoryStateBaseLayer::new(parent_state_raw);
378+
let parent_state = fetch_parent_state(ctx, parent_commitment)?;
379+
validate_block_inputs(ctx, block, parent_header, &parent_state)?;
353380

354381
// Execute and extract outputs
355382
let (write_batch, indexer_writes, logs) =

‎crates/chain-worker/src/tests/apply_checkpoint.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ use strata_asm_checkpoint_types::{
2929
use strata_asm_common::AsmManifest;
3030
use strata_checkpoint_types::EpochSummary;
3131
use strata_codec::encode_to_vec;
32+
use strata_db_types::DbResult;
3233
use strata_identifiers::{
33-
BRIDGE_GATEWAY_ACCT_SERIAL, Buf32, Epoch, EpochCommitment, OLBlockCommitment, OLBlockId,
34+
BRIDGE_GATEWAY_ACCT_SERIAL, Buf32, Epoch, EpochCommitment, L1Height, OLBlockCommitment,
35+
OLBlockId,
3436
};
3537
use strata_ol_chain_types_v1::{
3638
MAX_SEALING_MANIFEST_COUNT, OLBlockHeaderV1, OLBlockV1, OLLog as ChainOLLog,
@@ -76,6 +78,16 @@ impl MockChainWorkerContext {
7678
}
7779

7880
impl ChainWorkerContext for MockChainWorkerContext {
81+
fn l1_reorg_safe_depth(&self) -> u32 {
82+
1
83+
}
84+
fn canonical_l1_tip_height(&self) -> DbResult<Option<L1Height>> {
85+
Ok(self.manifests.keys().max().copied())
86+
}
87+
fn canonical_manifest(&self, height: L1Height) -> DbResult<Option<AsmManifest>> {
88+
Ok(self.manifests.get(&height).cloned())
89+
}
90+
7991
fn runtime_params(&self) -> OLRuntimeParams {
8092
self.runtime_params
8193
}

0 commit comments

Comments
 (0)