Skip to content

Commit fdf7d37

Browse files
committed
evm-ee: defer state root verification to merge to avoid state clone in zkVM
1 parent 00a9395 commit fdf7d37

File tree

9 files changed

+78
-60
lines changed

9 files changed

+78
-60
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ reth-cli = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
314314
reth-cli-commands = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
315315
reth-cli-runner = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
316316
reth-cli-util = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
317+
reth-consensus-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
317318
reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
318319
reth-engine-local = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }
319320
reth-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" }

crates/asm/manifest-types/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ edition = "2024"
66
[dependencies]
77
borsh.workspace = true
88
serde.workspace = true
9-
thiserror.workspace = true
109
strata-codec.workspace = true
1110
strata-identifiers.workspace = true
1211
strata-msg-fmt.workspace = true
12+
thiserror.workspace = true
1313

1414
[lints]
1515
workspace = true

crates/evm-ee/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ rsp-mpt.workspace = true
1818
alpen-reth-evm.workspace = true
1919
alpen-reth-primitives.workspace = true
2020
reth-chainspec.workspace = true
21+
reth-consensus-common.workspace = true
2122
reth-errors.workspace = true
2223
reth-evm.workspace = true
2324
reth-evm-ethereum.workspace = true

crates/evm-ee/src/execution.rs

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ use std::sync::Arc;
88
use alloy_consensus::Header;
99
use alpen_reth_evm::{accumulate_logs_bloom, evm::AlpenEvmFactory, extract_withdrawal_intents};
1010
use reth_chainspec::ChainSpec;
11+
use reth_consensus_common::validation::validate_body_against_header;
1112
use reth_evm::execute::{BasicBlockExecutor, Executor};
1213
use reth_evm_ethereum::EthEvmConfig;
1314
use reth_primitives::EthPrimitives;
1415
use revm::database::WrapDatabaseRef;
1516
use rsp_client_executor::BlockValidator;
1617
use strata_acct_types::{AccountId, BitcoinAmount, MsgPayload, SentMessage};
1718
use strata_ee_acct_types::{
18-
BlockAssembler, EnvError, EnvResult, ExecBlockOutput, ExecHeader, ExecPayload,
19+
BlockAssembler, EnvError, EnvResult, ExecBlockOutput, ExecPartialState, ExecPayload,
1920
ExecutionEnvironment,
2021
};
2122
use strata_ee_chain_types::{BlockInputs, BlockOutputs};
@@ -93,7 +94,11 @@ impl ExecutionEnvironment for EvmExecutionEnvironment {
9394
)
9495
.map_err(|_| EnvError::InvalidBlock)?;
9596

96-
// Step 2a: Validate deposits from BlockInputs against block withdrawals
97+
// Step 2b: Validate body against header (transactions_root, ommers_hash, withdrawals_root)
98+
validate_body_against_header(block.body(), block.header())
99+
.map_err(|_| EnvError::InvalidBlock)?;
100+
101+
// Step 2c: Validate deposits from BlockInputs against block withdrawals
97102
// The withdrawals header field is hijacked to represent deposits from the OL.
98103
// We need to ensure the authenticated deposits from BlockInputs match what's in the block.
99104
validate_deposits_against_block(&block, inputs)?;
@@ -129,14 +134,17 @@ impl ExecutionEnvironment for EvmExecutionEnvironment {
129134
extract_withdrawal_intents(&transactions, &execution_output.receipts).collect();
130135

131136
// Step 9: Convert execution outcome to HashedPostState
132-
let block_number = exec_payload.header_intrinsics().number;
133-
let hashed_post_state = compute_hashed_post_state(execution_output, block_number);
137+
let header_intrinsics = exec_payload.header_intrinsics();
138+
let hashed_post_state =
139+
compute_hashed_post_state(execution_output, header_intrinsics.number);
134140

135-
// Step 10: Compute state root
136-
let state_root = pre_state.compute_state_root_with_changes(&hashed_post_state);
141+
// Step 10: Get state root from header intrinsics (verification happens during merge)
142+
// This avoids an expensive state clone that would be needed to compute the root here.
143+
let intrinsics_state_root = header_intrinsics.state_root;
137144

138-
// Step 11: Create WriteBatch with computed metadata
139-
let write_batch = EvmWriteBatch::new(hashed_post_state, state_root.into(), logs_bloom);
145+
// Step 11: Create WriteBatch with intrinsics state root (to be verified during merge)
146+
let write_batch =
147+
EvmWriteBatch::new(hashed_post_state, intrinsics_state_root.into(), logs_bloom);
140148

141149
// Step 12: Create BlockOutputs with withdrawal intent messages
142150
let mut outputs = BlockOutputs::new_empty();
@@ -147,22 +155,16 @@ impl ExecutionEnvironment for EvmExecutionEnvironment {
147155

148156
fn verify_outputs_against_header(
149157
&self,
150-
header: &<Self::Block as strata_ee_acct_types::ExecBlock>::Header,
151-
outputs: &ExecBlockOutput<Self>,
158+
_header: &<Self::Block as strata_ee_acct_types::ExecBlock>::Header,
159+
_outputs: &ExecBlockOutput<Self>,
152160
) -> EnvResult<()> {
153-
// Verify that the outputs match what's committed in the header
154-
155-
// Check state root matches
156-
let computed_state_root = outputs.write_batch().state_root();
157-
let header_state_root = header.get_state_root();
158-
159-
if computed_state_root != header_state_root {
160-
//FIXME: is it correct enum to represent this error?
161-
return Err(EnvError::MismatchedCurStateData);
162-
}
163-
164-
// Note: transactions_root and receipts_root are verified during execution
165-
// by validate_block_post_execution() in execute_block_body()
161+
// State root verification is deferred to merge_write_into_state to avoid
162+
// an expensive state clone. The actual verification happens when the state
163+
// is mutated and we can compute the root directly without cloning.
164+
//
165+
// Note: The following are verified during execution in execute_block_body():
166+
// - transactions_root, ommers_hash, withdrawals_root: by validate_body_against_header()
167+
// - receipts_root, logs_bloom, gas_used: by validate_block_post_execution()
166168

167169
Ok(())
168170
}
@@ -174,6 +176,16 @@ impl ExecutionEnvironment for EvmExecutionEnvironment {
174176
) -> EnvResult<()> {
175177
// Merge the HashedPostState into the EthereumState
176178
state.merge_write_batch(wb);
179+
180+
// Verify state root AFTER merge (avoids expensive clone that would be needed
181+
// to compute the root before mutation)
182+
let computed_state_root = state.compute_state_root()?;
183+
let intrinsics_state_root = wb.intrinsics_state_root();
184+
185+
if computed_state_root != intrinsics_state_root {
186+
return Err(EnvError::InvalidBlock);
187+
}
188+
177189
Ok(())
178190
}
179191
}
@@ -185,7 +197,7 @@ impl BlockAssembler for EvmExecutionEnvironment {
185197
output: &ExecBlockOutput<Self>,
186198
) -> EnvResult<<Self::Block as strata_ee_acct_types::ExecBlock>::Header> {
187199
let intrinsics = exec_payload.header_intrinsics();
188-
let state_root = output.write_batch().state_root();
200+
let state_root = output.write_batch().intrinsics_state_root();
189201
let logs_bloom = output.write_batch().logs_bloom();
190202

191203
let header = Header {

crates/evm-ee/src/types/partial_state.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,19 +167,6 @@ impl EvmPartialState {
167167
WitnessDB::new(&self.ethereum_state, &self.block_hashes, &self.bytecodes)
168168
}
169169

170-
/// Computes the new state root by merging hashed post state changes into this state.
171-
///
172-
/// This clones the current state, applies the changes from the hashed post state,
173-
/// and computes the resulting state root.
174-
pub fn compute_state_root_with_changes(
175-
&self,
176-
hashed_post_state: &reth_trie::HashedPostState,
177-
) -> revm_primitives::B256 {
178-
let mut updated_state = self.ethereum_state.clone();
179-
updated_state.update(hashed_post_state);
180-
updated_state.state_root()
181-
}
182-
183170
/// Merges a write batch into this state by applying the hashed post state changes.
184171
///
185172
/// This updates the internal EthereumState with the changes from the write batch.

crates/evm-ee/src/types/tests.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,19 +224,22 @@ fn test_evm_write_batch_codec_roundtrip() {
224224

225225
// Create a simple write batch with default values
226226
let hashed_post_state = HashedPostState::default();
227-
let state_root = [42u8; 32];
227+
let intrinsics_state_root = [42u8; 32];
228228
let logs_bloom = Bloom::from([0xAB; 256]);
229229

230-
let write_batch = EvmWriteBatch::new(hashed_post_state, state_root, logs_bloom);
230+
let write_batch = EvmWriteBatch::new(hashed_post_state, intrinsics_state_root, logs_bloom);
231231

232232
// Encode
233233
let encoded = encode_to_vec(&write_batch).expect("encode failed");
234234

235235
// Decode
236236
let decoded: EvmWriteBatch = decode_buf_exact(&encoded).expect("decode failed");
237237

238-
// Verify state_root matches
239-
assert_eq!(decoded.state_root(), write_batch.state_root());
238+
// Verify intrinsics_state_root matches
239+
assert_eq!(
240+
decoded.intrinsics_state_root(),
241+
write_batch.intrinsics_state_root()
242+
);
240243

241244
// Verify logs_bloom matches
242245
assert_eq!(decoded.logs_bloom(), write_batch.logs_bloom());

crates/evm-ee/src/types/write_batch.rs

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,33 @@ use crate::codec_shims::{decode_hashed_post_state, encode_hashed_post_state};
1313
/// in account states and storage slots after executing a block. It's used to
1414
/// apply state changes to the sparse EthereumState.
1515
///
16-
/// Also stores execution metadata (state root, logs bloom) needed for completing
17-
/// the block header.
16+
/// Also stores execution metadata (state root from header intrinsics, logs bloom)
17+
/// needed for block header completion and state root verification during merge.
1818
#[derive(Clone, Debug)]
1919
pub struct EvmWriteBatch {
2020
hashed_post_state: HashedPostState,
21-
/// The computed state root after applying the state changes
22-
state_root: Hash,
21+
/// The state root extracted from block header intrinsics.
22+
///
23+
/// This value is taken directly from `header_intrinsics.state_root` during
24+
/// block execution, NOT computed from the pre-state. Actual verification
25+
/// occurs in `merge_write_into_state` after the state is mutated, where
26+
/// we compute the real state root and compare it against this value.
27+
/// This approach avoids an expensive state clone in zkVM.
28+
intrinsics_state_root: Hash,
2329
/// The accumulated logs bloom from all receipts
2430
logs_bloom: Bloom,
2531
}
2632

2733
impl EvmWriteBatch {
28-
/// Creates a new EvmWriteBatch from a HashedPostState and computed metadata.
29-
pub fn new(hashed_post_state: HashedPostState, state_root: Hash, logs_bloom: Bloom) -> Self {
34+
/// Creates a new EvmWriteBatch from a HashedPostState and header intrinsics metadata.
35+
pub fn new(
36+
hashed_post_state: HashedPostState,
37+
intrinsics_state_root: Hash,
38+
logs_bloom: Bloom,
39+
) -> Self {
3040
Self {
3141
hashed_post_state,
32-
state_root,
42+
intrinsics_state_root,
3343
logs_bloom,
3444
}
3545
}
@@ -39,9 +49,12 @@ impl EvmWriteBatch {
3949
&self.hashed_post_state
4050
}
4151

42-
/// Gets the computed state root.
43-
pub fn state_root(&self) -> Hash {
44-
self.state_root
52+
/// Gets the state root from block header intrinsics.
53+
///
54+
/// This value is verified against the actual computed state root
55+
/// during `merge_write_into_state`.
56+
pub fn intrinsics_state_root(&self) -> Hash {
57+
self.intrinsics_state_root
4558
}
4659

4760
/// Gets the accumulated logs bloom.
@@ -60,8 +73,8 @@ impl Codec for EvmWriteBatch {
6073
// Encode HashedPostState using custom deterministic encoding
6174
encode_hashed_post_state(&self.hashed_post_state, enc)?;
6275

63-
// Encode state_root (32 bytes)
64-
enc.write_buf(&self.state_root)?;
76+
// Encode intrinsics_state_root (32 bytes)
77+
enc.write_buf(&self.intrinsics_state_root)?;
6578

6679
// Encode logs_bloom (256 bytes)
6780
enc.write_buf(self.logs_bloom.as_slice())?;
@@ -73,9 +86,9 @@ impl Codec for EvmWriteBatch {
7386
// Decode HashedPostState using custom deterministic decoding
7487
let hashed_post_state = decode_hashed_post_state(dec)?;
7588

76-
// Decode state_root (32 bytes)
77-
let mut state_root = [0u8; 32];
78-
dec.read_buf(&mut state_root)?;
89+
// Decode intrinsics_state_root (32 bytes)
90+
let mut intrinsics_state_root = [0u8; 32];
91+
dec.read_buf(&mut intrinsics_state_root)?;
7992

8093
// Decode logs_bloom (256 bytes)
8194
let mut logs_bloom_bytes = [0u8; 256];
@@ -84,7 +97,7 @@ impl Codec for EvmWriteBatch {
8497

8598
Ok(Self {
8699
hashed_post_state,
87-
state_root,
100+
intrinsics_state_root,
88101
logs_bloom,
89102
})
90103
}

crates/ol/da/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ edition = "2024"
66
[dependencies]
77
strata-acct-types.workspace = true
88
strata-codec.workspace = true
9-
strata-ledger-types.workspace = true
109
strata-identifiers.workspace = true
10+
strata-ledger-types.workspace = true
1111

1212
thiserror.workspace = true
1313

0 commit comments

Comments
 (0)