Skip to content

Commit d9243cf

Browse files
authored
fix(chain-worker): validate canonical ASM manifests before execution
1 parent c9e0034 commit d9243cf

21 files changed

Lines changed: 545 additions & 30 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: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ 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, L1BlockCommitment, OLBlockCommitment, OLBlockId};
21+
use strata_identifiers::{
22+
AccountId, Hash, L1BlockCommitment, L1Height, OLBlockCommitment, OLBlockId,
23+
};
2124
use strata_msg_fmt::{Msg, MsgRef};
2225
use strata_node_context::NodeContext;
2326
use strata_ol_chain_types_v1::{
@@ -86,6 +89,7 @@ pub struct ChainWorkerContextImpl {
8689

8790
/// Runtime parameters for OL STF execution.
8891
runtime_params: OLRuntimeParams,
92+
l1_reorg_safe_depth: u32,
8993

9094
/// Runtime handle
9195
handle: Handle,
@@ -106,6 +110,7 @@ impl ChainWorkerContextImpl {
106110
epoch_summary_tx,
107111
ol_params: nodectx.ol_params().clone(),
108112
runtime_params: nodectx.ol_params().runtime_params(),
113+
l1_reorg_safe_depth: nodectx.config().btcio.l1_reorg_safe_depth,
109114
handle: nodectx.executor().handle().clone(),
110115
}
111116
}
@@ -124,6 +129,26 @@ impl ChainWorkerContextImpl {
124129
}
125130

126131
impl ChainWorkerContext for ChainWorkerContextImpl {
132+
fn l1_reorg_safe_depth(&self) -> u32 {
133+
self.l1_reorg_safe_depth
134+
}
135+
136+
fn canonical_l1_tip_height(&self) -> DbResult<Option<L1Height>> {
137+
self.l1_block_mgr
138+
.get_canonical_chain_tip()
139+
.map(|tip| tip.map(|(height, _)| height))
140+
}
141+
142+
fn canonical_manifest(&self, height: L1Height) -> DbResult<Option<AsmManifest>> {
143+
let Some(id) = self
144+
.l1_block_mgr
145+
.get_canonical_blockid_at_height_uncached(height)?
146+
else {
147+
return Ok(None);
148+
};
149+
self.l1_block_mgr.get_block_manifest(&id)
150+
}
151+
127152
fn runtime_params(&self) -> OLRuntimeParams {
128153
self.runtime_params
129154
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,28 @@ use thiserror::Error;
1515
/// Return type for worker messages.
1616
pub type WorkerResult<T> = Result<T, WorkerError>;
1717

18+
/// Explains why canonical ASM provenance cannot yet be established.
19+
#[derive(Clone, Copy, Debug)]
20+
pub enum ManifestPendingReason {
21+
MissingTip,
22+
NotBuried,
23+
MissingManifest,
24+
}
25+
1826
/// Errors that can occur during chain worker operations.
1927
#[derive(Debug, Error)]
2028
pub enum WorkerError {
29+
/// Canonical L1/ASM data does not yet validate a carried manifest.
30+
#[error("ASM manifest at height {height} is pending: {reason:?}")]
31+
ManifestPending {
32+
height: L1Height,
33+
reason: ManifestPendingReason,
34+
},
35+
36+
/// A canonical validation read failed; this is not an invalid block verdict.
37+
#[error("failed to read canonical ASM provenance: {0}")]
38+
ManifestStorage(#[source] DbError),
39+
2140
/// Block not found in database.
2241
#[error("missing OL block {0}")]
2342
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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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::{ExecError, OLSpecId, 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+
spec: OLSpecId,
16+
last_height: L1Height,
17+
manifests: &[AsmManifest],
18+
) -> WorkerResult<()> {
19+
// Validate every height first; a later malformed height is not a missing dependency.
20+
validate_manifest_heights(spec, last_height, manifests)?;
21+
22+
let Some(first) = manifests.first() else {
23+
return Ok(());
24+
};
25+
26+
let tip = ctx
27+
.canonical_l1_tip_height()
28+
.map_err(WorkerError::ManifestStorage)?
29+
.ok_or(WorkerError::ManifestPending {
30+
height: first.height(),
31+
reason: ManifestPendingReason::MissingTip,
32+
})?;
33+
let buried_tip = tip.checked_sub(ctx.l1_reorg_safe_depth().saturating_sub(1));
34+
35+
for manifest in manifests {
36+
let height = manifest.height();
37+
if buried_tip.is_none_or(|tip| height > tip) {
38+
return Err(WorkerError::ManifestPending {
39+
height,
40+
reason: ManifestPendingReason::NotBuried,
41+
});
42+
}
43+
44+
let canonical = ctx
45+
.canonical_manifest(height)
46+
.map_err(WorkerError::ManifestStorage)?
47+
.ok_or(WorkerError::ManifestPending {
48+
height,
49+
reason: ManifestPendingReason::MissingManifest,
50+
})?;
51+
52+
if canonical.compute_hash() != manifest.compute_hash() {
53+
return Err(ExecError::AsmManifestContentMismatch { height }.into());
54+
}
55+
}
56+
57+
Ok(())
58+
}

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

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ use strata_ol_state_types::{
3737
use strata_ol_state_types_v1::{IStateBatchApplicable, OLStateV1, WriteBatch};
3838
use strata_ol_stf::{
3939
BlockInfo, EpochDaReplayError, EpochInfo, EpochSpecSelectionError, OLSpecId, apply_da_epoch,
40-
select_next_epoch_spec, verify_block,
40+
select_next_epoch_spec, sequencer::verify_block_structure, verify_block,
41+
verify_header_continuity,
4142
};
4243
use strata_primitives::epoch::EpochCommitment;
4344
use strata_service::ServiceState;
@@ -48,6 +49,7 @@ use crate::{
4849
ChainWorkerContextImpl,
4950
errors::{WorkerError, WorkerResult},
5051
output::OLBlockExecutionOutput,
52+
provenance::validate_manifests,
5153
traits::ChainWorkerContext,
5254
};
5355

@@ -331,10 +333,44 @@ fn fetch_block_with_parent(
331333
Ok((block, parent_header, parent_commitment))
332334
}
333335

336+
/// Validates all block inputs without executing or persisting state.
337+
fn validate_block_inputs(
338+
spec: OLSpecId,
339+
ctx: &impl ChainWorkerContext,
340+
block: &OLBlockV1,
341+
parent_header: Option<&OLBlockHeaderV1>,
342+
parent_state: &impl IStateAccessor,
343+
) -> WorkerResult<()> {
344+
verify_header_continuity(spec, block.header(), parent_header)?;
345+
verify_block_structure(spec, block.header(), block.body())?;
346+
if let Some(container) = block.body().manifests() {
347+
validate_manifests(
348+
ctx,
349+
spec,
350+
parent_state.last_l1_height(),
351+
container.manifests(),
352+
)?;
353+
}
354+
Ok(())
355+
}
356+
357+
/// Loads the parent state used by input validation and STF execution, keeping its
358+
/// spec versions.
359+
fn fetch_parent_state(
360+
ctx: &impl ChainWorkerContext,
361+
parent_commitment: OLBlockCommitment,
362+
) -> WorkerResult<MemoryStateBaseLayer<OLStateV1>> {
363+
let parent_state = ctx
364+
.fetch_ol_state(parent_commitment)?
365+
.ok_or(WorkerError::MissingPreState(parent_commitment))?;
366+
Ok(MemoryStateBaseLayer::from_container(parent_state))
367+
}
368+
334369
/// Executes the STF on a block and returns the execution output.
335370
///
336371
/// This fetches parent state, builds the state stack, runs verification,
337372
/// and extracts the resulting write batch and indexer writes.
373+
/// Callers classify failures; pending canonical manifests are expected during L1 catch-up.
338374
#[instrument(
339375
skip_all,
340376
fields(
@@ -344,7 +380,6 @@ fn fetch_block_with_parent(
344380
is_terminal = block.header().is_terminal(),
345381
%parent_commitment,
346382
),
347-
err,
348383
)]
349384
fn execute_stf(
350385
ctx: &impl ChainWorkerContext,
@@ -354,12 +389,8 @@ fn execute_stf(
354389
parent_header: Option<&OLBlockHeaderV1>,
355390
parent_commitment: OLBlockCommitment,
356391
) -> WorkerResult<(OLBlockExecutionOutput, OLStateContainer)> {
357-
// Fetch parent state and build its state accessor, keeping its spec
358-
// versions.
359-
let parent_state_raw = ctx
360-
.fetch_ol_state(parent_commitment)?
361-
.ok_or(WorkerError::MissingPreState(parent_commitment))?;
362-
let parent_state = MemoryStateBaseLayer::from_container(parent_state_raw);
392+
let parent_state = fetch_parent_state(ctx, parent_commitment)?;
393+
validate_block_inputs(spec, ctx, block, parent_header, &parent_state)?;
363394

364395
// Execute and extract outputs
365396
let (write_batch, indexer_writes, logs) =

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ 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, L1BlockCommitment,
34+
BRIDGE_GATEWAY_ACCT_SERIAL, Buf32, Epoch, EpochCommitment, L1BlockCommitment, L1Height,
3435
OLBlockCommitment, OLBlockId,
3536
};
3637
use strata_ol_chain_types_v1::{
@@ -82,6 +83,16 @@ impl MockChainWorkerContext {
8283
}
8384

8485
impl ChainWorkerContext for MockChainWorkerContext {
86+
fn l1_reorg_safe_depth(&self) -> u32 {
87+
1
88+
}
89+
fn canonical_l1_tip_height(&self) -> DbResult<Option<L1Height>> {
90+
Ok(self.manifests.keys().max().copied())
91+
}
92+
fn canonical_manifest(&self, height: L1Height) -> DbResult<Option<AsmManifest>> {
93+
Ok(self.manifests.get(&height).cloned())
94+
}
95+
8596
fn runtime_params(&self) -> OLRuntimeParams {
8697
self.runtime_params
8798
}

0 commit comments

Comments
 (0)