From 8094653e1e75252425cd557b6114666669d18cee Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Sat, 4 Apr 2026 18:11:47 -0300 Subject: [PATCH 1/9] feat(trie): introduce ExecutionWitnessMode and update witness methods to support it --- crates/chain-state/src/in_memory.rs | 1 + crates/chain-state/src/memory_overlay.rs | 9 +++++++-- .../engine/execution-cache/src/cached_state.rs | 3 ++- .../engine/invalid-block-hooks/src/witness.rs | 1 + .../engine/tree/src/tree/instrumented_state.rs | 3 ++- crates/revm/src/test_utils.rs | 7 ++++++- crates/rpc/rpc-api/Cargo.toml | 2 +- crates/rpc/rpc-eth-types/src/cache/db.rs | 3 ++- crates/storage/provider/src/test_utils/mock.rs | 7 ++++++- crates/storage/rpc-provider/src/lib.rs | 1 + crates/storage/storage-api/src/macros.rs | 2 +- crates/storage/storage-api/src/noop.rs | 11 ++++++++--- crates/storage/storage-api/src/trie.rs | 13 +++++++++---- crates/trie/common/src/execution_witness.rs | 18 ++++++++++++++++++ crates/trie/common/src/lib.rs | 3 +++ 15 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 crates/trie/common/src/execution_witness.rs diff --git a/crates/chain-state/src/in_memory.rs b/crates/chain-state/src/in_memory.rs index 8d170053b19..ecdece9a337 100644 --- a/crates/chain-state/src/in_memory.rs +++ b/crates/chain-state/src/in_memory.rs @@ -1169,6 +1169,7 @@ mod tests { &self, _input: TrieInput, _target: HashedPostState, + _mode: reth_trie::ExecutionWitnessMode, ) -> ProviderResult> { Ok(Vec::default()) } diff --git a/crates/chain-state/src/memory_overlay.rs b/crates/chain-state/src/memory_overlay.rs index a4269886da6..7e31ec06fee 100644 --- a/crates/chain-state/src/memory_overlay.rs +++ b/crates/chain-state/src/memory_overlay.rs @@ -197,9 +197,14 @@ impl StateProofProvider for MemoryOverlayStateProviderRef<'_, self.historical.multiproof(input, targets) } - fn witness(&self, mut input: TrieInput, target: HashedPostState) -> ProviderResult> { + fn witness( + &self, + mut input: TrieInput, + target: HashedPostState, + mode: reth_trie::ExecutionWitnessMode, + ) -> ProviderResult> { input.prepend_self(self.trie_input().clone()); - self.historical.witness(input, target) + self.historical.witness(input, target, mode) } } diff --git a/crates/engine/execution-cache/src/cached_state.rs b/crates/engine/execution-cache/src/cached_state.rs index 3dff646d5f7..3acc92a2810 100644 --- a/crates/engine/execution-cache/src/cached_state.rs +++ b/crates/engine/execution-cache/src/cached_state.rs @@ -566,8 +566,9 @@ impl StateProofProvider &self, input: TrieInput, target: HashedPostState, + mode: reth_trie::ExecutionWitnessMode, ) -> ProviderResult> { - self.state_provider.witness(input, target) + self.state_provider.witness(input, target, mode) } } diff --git a/crates/engine/invalid-block-hooks/src/witness.rs b/crates/engine/invalid-block-hooks/src/witness.rs index c068220da8b..0a8db16f621 100644 --- a/crates/engine/invalid-block-hooks/src/witness.rs +++ b/crates/engine/invalid-block-hooks/src/witness.rs @@ -239,6 +239,7 @@ where DebugApiClient::<()>::debug_execution_witness( healthy_node_client, block_number.into(), + None, ) .await })?; diff --git a/crates/engine/tree/src/tree/instrumented_state.rs b/crates/engine/tree/src/tree/instrumented_state.rs index b161fbd9fbd..d7f0d2ea1bf 100644 --- a/crates/engine/tree/src/tree/instrumented_state.rs +++ b/crates/engine/tree/src/tree/instrumented_state.rs @@ -237,8 +237,9 @@ impl StateProofProvider for InstrumentedStateProvider &self, input: TrieInput, target: HashedPostState, + mode: reth_trie::ExecutionWitnessMode, ) -> ProviderResult> { - self.state_provider.witness(input, target) + self.state_provider.witness(input, target, mode) } } diff --git a/crates/revm/src/test_utils.rs b/crates/revm/src/test_utils.rs index b2688084610..abd861e4414 100644 --- a/crates/revm/src/test_utils.rs +++ b/crates/revm/src/test_utils.rs @@ -141,7 +141,12 @@ impl StateProofProvider for StateProviderTest { unimplemented!("proof generation is not supported") } - fn witness(&self, _input: TrieInput, _target: HashedPostState) -> ProviderResult> { + fn witness( + &self, + _input: TrieInput, + _target: HashedPostState, + _mode: reth_trie::ExecutionWitnessMode, + ) -> ProviderResult> { unimplemented!("witness generation is not supported") } } diff --git a/crates/rpc/rpc-api/Cargo.toml b/crates/rpc/rpc-api/Cargo.toml index bd215d62129..a0d0ca941ab 100644 --- a/crates/rpc/rpc-api/Cargo.toml +++ b/crates/rpc/rpc-api/Cargo.toml @@ -16,7 +16,7 @@ workspace = true reth-rpc-eth-api.workspace = true reth-engine-primitives.workspace = true reth-network-peers.workspace = true -reth-trie-common.workspace = true +reth-trie-common = { workspace = true, features = ["serde"] } reth-chain-state.workspace = true # ethereum diff --git a/crates/rpc/rpc-eth-types/src/cache/db.rs b/crates/rpc/rpc-eth-types/src/cache/db.rs index 09e1b3db3c1..167320dd823 100644 --- a/crates/rpc/rpc-eth-types/src/cache/db.rs +++ b/crates/rpc/rpc-eth-types/src/cache/db.rs @@ -101,8 +101,9 @@ impl reth_storage_api::StateProofProvider for StateProviderTraitObjWrapper { &self, input: reth_trie::TrieInput, target: reth_trie::HashedPostState, + mode: reth_trie::ExecutionWitnessMode, ) -> reth_errors::ProviderResult> { - self.0.witness(input, target) + self.0.witness(input, target, mode) } } diff --git a/crates/storage/provider/src/test_utils/mock.rs b/crates/storage/provider/src/test_utils/mock.rs index 8f2fd93c5e2..2d6c3b422e1 100644 --- a/crates/storage/provider/src/test_utils/mock.rs +++ b/crates/storage/provider/src/test_utils/mock.rs @@ -857,7 +857,12 @@ where Ok(MultiProof::default()) } - fn witness(&self, _input: TrieInput, _target: HashedPostState) -> ProviderResult> { + fn witness( + &self, + _input: TrieInput, + _target: HashedPostState, + _mode: reth_trie::ExecutionWitnessMode, + ) -> ProviderResult> { Ok(Vec::default()) } } diff --git a/crates/storage/rpc-provider/src/lib.rs b/crates/storage/rpc-provider/src/lib.rs index e8b5b54321c..0e248a9a030 100644 --- a/crates/storage/rpc-provider/src/lib.rs +++ b/crates/storage/rpc-provider/src/lib.rs @@ -1303,6 +1303,7 @@ where &self, _input: TrieInput, _target: HashedPostState, + _mode: reth_trie::ExecutionWitnessMode, ) -> Result, ProviderError> { Err(ProviderError::UnsupportedProvider) } diff --git a/crates/storage/storage-api/src/macros.rs b/crates/storage/storage-api/src/macros.rs index a299c529b89..c8eb230ecaa 100644 --- a/crates/storage/storage-api/src/macros.rs +++ b/crates/storage/storage-api/src/macros.rs @@ -59,7 +59,7 @@ macro_rules! delegate_provider_impls { StateProofProvider $(where [$($generics)*])? { fn proof(&self, input: reth_trie::TrieInput, address: alloy_primitives::Address, slots: &[alloy_primitives::B256]) -> reth_storage_api::errors::provider::ProviderResult; fn multiproof(&self, input: reth_trie::TrieInput, targets: reth_trie::MultiProofTargets) -> reth_storage_api::errors::provider::ProviderResult; - fn witness(&self, input: reth_trie::TrieInput, target: reth_trie::HashedPostState) -> reth_storage_api::errors::provider::ProviderResult>; + fn witness(&self, input: reth_trie::TrieInput, target: reth_trie::HashedPostState, mode: reth_trie::ExecutionWitnessMode) -> reth_storage_api::errors::provider::ProviderResult>; } HashedPostStateProvider $(where [$($generics)*])? { fn hashed_post_state(&self, bundle_state: &revm_database::BundleState) -> reth_trie::HashedPostState; diff --git a/crates/storage/storage-api/src/noop.rs b/crates/storage/storage-api/src/noop.rs index 8f5b3e340dd..488e463d0b5 100644 --- a/crates/storage/storage-api/src/noop.rs +++ b/crates/storage/storage-api/src/noop.rs @@ -35,8 +35,8 @@ use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_storage_errors::provider::{ProviderError, ProviderResult}; use reth_trie_common::{ - updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof, - MultiProofTargets, StorageMultiProof, StorageProof, TrieInput, + updates::TrieUpdates, AccountProof, ExecutionWitnessMode, HashedPostState, HashedStorage, + MultiProof, MultiProofTargets, StorageMultiProof, StorageProof, TrieInput, }; /// Supports various api interfaces for testing purposes. @@ -504,7 +504,12 @@ impl StateProofProvider for NoopProvider ProviderResult> { + fn witness( + &self, + _input: TrieInput, + _target: HashedPostState, + _mode: ExecutionWitnessMode, + ) -> ProviderResult> { Ok(Vec::default()) } } diff --git a/crates/storage/storage-api/src/trie.rs b/crates/storage/storage-api/src/trie.rs index b462a3d1863..b5c7de7b652 100644 --- a/crates/storage/storage-api/src/trie.rs +++ b/crates/storage/storage-api/src/trie.rs @@ -3,8 +3,8 @@ use alloy_primitives::{Address, Bytes, B256}; use reth_storage_errors::provider::ProviderResult; use reth_trie_common::{ updates::{StorageTrieUpdatesSorted, TrieUpdates, TrieUpdatesSorted}, - AccountProof, HashedPostState, HashedStorage, MultiProof, MultiProofTargets, StorageMultiProof, - StorageProof, TrieInput, + AccountProof, ExecutionWitnessMode, HashedPostState, HashedStorage, MultiProof, + MultiProofTargets, StorageMultiProof, StorageProof, TrieInput, }; /// A type that can compute the state root of a given post state. @@ -85,8 +85,13 @@ pub trait StateProofProvider { targets: MultiProofTargets, ) -> ProviderResult; - /// Get trie witness for provided state. - fn witness(&self, input: TrieInput, target: HashedPostState) -> ProviderResult>; + /// Get trie witness for provided state using the given witness generation mode. + fn witness( + &self, + input: TrieInput, + target: HashedPostState, + mode: ExecutionWitnessMode, + ) -> ProviderResult>; } /// Trie Writer diff --git a/crates/trie/common/src/execution_witness.rs b/crates/trie/common/src/execution_witness.rs new file mode 100644 index 00000000000..d08df72f8fb --- /dev/null +++ b/crates/trie/common/src/execution_witness.rs @@ -0,0 +1,18 @@ +/// Controls how execution witnesses are generated. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +pub enum ExecutionWitnessMode { + /// Preserve the current witness shape for compatibility with existing consumers. + #[default] + Legacy, + /// Produce the minimized, draft-spec witness form. + Canonical, +} + +impl ExecutionWitnessMode { + /// Returns `true` if the mode is [`Self::Canonical`]. + pub const fn is_canonical(self) -> bool { + matches!(self, Self::Canonical) + } +} diff --git a/crates/trie/common/src/lib.rs b/crates/trie/common/src/lib.rs index e313dd1b526..1ac046d5e0d 100644 --- a/crates/trie/common/src/lib.rs +++ b/crates/trie/common/src/lib.rs @@ -11,6 +11,9 @@ extern crate alloc; +mod execution_witness; +pub use execution_witness::ExecutionWitnessMode; + /// Lazy initialization wrapper for trie data. mod lazy; pub use lazy::{LazyTrieData, SortedTrieData}; From 163f656f475888ff40bb24a12a086c1387733464 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Sat, 4 Apr 2026 18:37:10 -0300 Subject: [PATCH 2/9] feat(witness): add ExecutionWitnessMode support for state recording and retrieval through RPC --- crates/revm/src/witness.rs | 56 ++++++++++------ crates/rpc/rpc-api/src/debug.rs | 16 +++-- crates/rpc/rpc/src/debug.rs | 65 +++++-------------- .../src/providers/state/historical.rs | 29 ++++++--- .../provider/src/providers/state/latest.rs | 26 +++++--- 5 files changed, 102 insertions(+), 90 deletions(-) diff --git a/crates/revm/src/witness.rs b/crates/revm/src/witness.rs index 968e26bc83f..e5e05a0e54d 100644 --- a/crates/revm/src/witness.rs +++ b/crates/revm/src/witness.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; -use alloy_primitives::{keccak256, Bytes, B256}; -use reth_trie::{HashedPostState, HashedStorage}; +use alloy_primitives::{keccak256, map::B256Map, Bytes, B256}; +use reth_trie::{ExecutionWitnessMode, HashedPostState, HashedStorage}; use revm::database::State; /// Tracks state changes during execution. @@ -30,21 +30,36 @@ pub struct ExecutionWitnessRecord { } impl ExecutionWitnessRecord { - /// Records the state after execution. - pub fn record_executed_state(&mut self, statedb: &State) { - self.codes = statedb - .cache - .contracts - .values() - .map(|code| code.original_bytes()) - .chain( - // cache state does not have all the contracts, especially when - // a contract is created within the block - // the contract only exists in bundle state, therefore we need - // to include them as well - statedb.bundle_state.contracts.values().map(|code| code.original_bytes()), - ) - .collect(); + /// Records the state after execution using the given witness generation mode. + pub fn record_executed_state(&mut self, statedb: &State, mode: ExecutionWitnessMode) { + self.codes = match mode { + ExecutionWitnessMode::Legacy => statedb + .cache + .contracts + .values() + .map(|code| code.original_bytes()) + .chain( + // cache state does not have all the contracts, especially when + // a contract is created within the block + // the contract only exists in bundle state, therefore we need + // to include them as well + statedb.bundle_state.contracts.values().map(|code| code.original_bytes()), + ) + .collect(), + ExecutionWitnessMode::Canonical => { + let mut accessed_codes = B256Map::default(); + for code in statedb.cache.contracts.values() { + let code = code.original_bytes(); + if code.is_empty() { + continue; + } + accessed_codes.entry(keccak256(&code)).or_insert(code); + } + let mut codes: Vec<_> = accessed_codes.into_values().collect(); + codes.sort_unstable(); + codes + } + }; for (address, account) in &statedb.cache.accounts { let hashed_address = keccak256(address); @@ -75,9 +90,9 @@ impl ExecutionWitnessRecord { } /// Creates the record from the state after execution. - pub fn from_executed_state(state: &State) -> Self { + pub fn from_executed_state(state: &State, mode: ExecutionWitnessMode) -> Self { let mut record = Self::default(); - record.record_executed_state(state); + record.record_executed_state(state, mode); record } @@ -93,6 +108,7 @@ impl ExecutionWitnessRecord { state_provider: &SP, headers_provider: &HP, block_number: u64, + mode: ExecutionWitnessMode, ) -> reth_storage_errors::provider::ProviderResult where SP: reth_storage_api::StateProofProvider + ?Sized, @@ -101,7 +117,7 @@ impl ExecutionWitnessRecord { { let Self { hashed_state, codes, keys, lowest_block_number } = self; - let state = state_provider.witness(Default::default(), hashed_state)?; + let state = state_provider.witness(Default::default(), hashed_state, mode)?; let mut exec_witness = alloy_rpc_types_debug::ExecutionWitness { state, codes, keys, ..Default::default() }; diff --git a/crates/rpc/rpc-api/src/debug.rs b/crates/rpc/rpc-api/src/debug.rs index 0093938f520..7571340191c 100644 --- a/crates/rpc/rpc-api/src/debug.rs +++ b/crates/rpc/rpc-api/src/debug.rs @@ -9,7 +9,7 @@ use alloy_rpc_types_trace::geth::{ BlockTraceResult, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, TraceResult, }; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use reth_trie_common::{updates::TrieUpdates, HashedPostState}; +use reth_trie_common::{updates::TrieUpdates, ExecutionWitnessMode, HashedPostState}; /// Debug rpc interface. #[cfg_attr(not(feature = "client"), rpc(server, namespace = "debug"))] @@ -140,21 +140,27 @@ pub trait DebugApi { /// to their preimages that were required during the execution of the block, including during /// state root recomputation. /// - /// The first argument is the block number or tag. + /// The first argument is the block number or tag. The optional second argument selects the + /// witness generation mode and defaults to `legacy`. #[method(name = "executionWitness")] - async fn debug_execution_witness(&self, block: BlockNumberOrTag) - -> RpcResult; + async fn debug_execution_witness( + &self, + block: BlockNumberOrTag, + mode: Option, + ) -> RpcResult; /// The `debug_executionWitnessByBlockHash` method allows for re-execution of a block with the /// purpose of generating an execution witness. The witness comprises of a map of all hashed /// trie nodes to their preimages that were required during the execution of the block, /// including during state root recomputation. /// - /// The first argument is the block hash. + /// The first argument is the block hash. The optional second argument selects the witness + /// generation mode and defaults to `legacy`. #[method(name = "executionWitnessByBlockHash")] async fn debug_execution_witness_by_block_hash( &self, hash: B256, + mode: Option, ) -> RpcResult; /// Re-executes a block and returns the Block Access List (BAL) as defined in EIP-7928. diff --git a/crates/rpc/rpc/src/debug.rs b/crates/rpc/rpc/src/debug.rs index ad186073d27..a65bb046ef0 100644 --- a/crates/rpc/rpc/src/debug.rs +++ b/crates/rpc/rpc/src/debug.rs @@ -33,11 +33,10 @@ use reth_rpc_eth_types::EthApiError; use reth_rpc_server_types::{result::internal_rpc_err, ToRpcResult}; use reth_storage_api::{ BlockIdReader, BlockReaderIdExt, HashedPostStateProvider, HeaderProvider, ProviderBlock, - ReceiptProviderIdExt, StateProofProvider, StateProviderFactory, StateRootProvider, - TransactionVariant, + ReceiptProviderIdExt, StateProviderFactory, StateRootProvider, TransactionVariant, }; use reth_tasks::{pool::BlockingTaskGuard, Runtime}; -use reth_trie_common::{updates::TrieUpdates, HashedPostState}; +use reth_trie_common::{updates::TrieUpdates, ExecutionWitnessMode, HashedPostState}; use revm::{database::states::bundle_state::BundleRetention, DatabaseCommit}; use revm_inspectors::tracing::{DebugInspector, TransactionContext}; use serde::{Deserialize, Serialize}; @@ -498,6 +497,7 @@ where pub async fn debug_execution_witness_by_block_hash( &self, hash: B256, + mode: Option, ) -> Result { let this = self.clone(); let block = this @@ -506,7 +506,7 @@ where .await? .ok_or(EthApiError::HeaderNotFound(hash.into()))?; - self.debug_execution_witness_for_block(block).await + self.debug_execution_witness_for_block(block, mode.unwrap_or_default()).await } /// The `debug_executionWitness` method allows for re-execution of a block with the purpose of @@ -516,6 +516,7 @@ where pub async fn debug_execution_witness( &self, block_id: BlockNumberOrTag, + mode: Option, ) -> Result { let this = self.clone(); let block = this @@ -524,18 +525,17 @@ where .await? .ok_or(EthApiError::HeaderNotFound(block_id.into()))?; - self.debug_execution_witness_for_block(block).await + self.debug_execution_witness_for_block(block, mode.unwrap_or_default()).await } /// Generates an execution witness, using the given recovered block. pub async fn debug_execution_witness_for_block( &self, block: Arc>>, + mode: ExecutionWitnessMode, ) -> Result { let block_number = block.header().number(); - - let (mut exec_witness, lowest_block_number) = self - .eth_api() + self.eth_api() .spawn_with_state_at_block(block.parent_hash(), move |eth_api, mut db| { let block_executor = eth_api.evm_config().executor(&mut db); @@ -543,48 +543,15 @@ where let _ = block_executor .execute_with_state_closure(&block, |statedb: &State<_>| { - witness_record.record_executed_state(statedb); + witness_record.record_executed_state(statedb, mode); }) .map_err(|err| EthApiError::Internal(err.into()))?; - let ExecutionWitnessRecord { hashed_state, codes, keys, lowest_block_number } = - witness_record; - - let state = db - .database - .0 - .witness(Default::default(), hashed_state) - .map_err(EthApiError::from)?; - Ok(( - ExecutionWitness { state, codes, keys, ..Default::default() }, - lowest_block_number, - )) + Ok(witness_record + .into_execution_witness(&db.database.0, eth_api.provider(), block_number, mode) + .map_err(EthApiError::from)?) }) - .await?; - - let smallest = match lowest_block_number { - Some(smallest) => smallest, - None => { - // Return only the parent header, if there were no calls to the - // BLOCKHASH opcode. - block_number.saturating_sub(1) - } - }; - - let range = smallest..block_number; - exec_witness.headers = self - .provider() - .headers_range(range) - .map_err(EthApiError::from)? - .into_iter() - .map(|header| { - let mut serialized_header = Vec::new(); - header.encode(&mut serialized_header); - serialized_header.into() - }) - .collect(); - - Ok(exec_witness) + .await } /// Returns the code associated with a given hash at the specified block ID. If no code is @@ -846,18 +813,20 @@ where async fn debug_execution_witness( &self, block: BlockNumberOrTag, + mode: Option, ) -> RpcResult { let _permit = self.acquire_trace_permit().await; - Self::debug_execution_witness(self, block).await.map_err(Into::into) + Self::debug_execution_witness(self, block, mode).await.map_err(Into::into) } /// Handler for `debug_executionWitnessByBlockHash` async fn debug_execution_witness_by_block_hash( &self, hash: B256, + mode: Option, ) -> RpcResult { let _permit = self.acquire_trace_permit().await; - Self::debug_execution_witness_by_block_hash(self, hash).await.map_err(Into::into) + Self::debug_execution_witness_by_block_hash(self, hash, mode).await.map_err(Into::into) } async fn debug_get_block_access_list(&self, _block_id: BlockId) -> RpcResult { diff --git a/crates/storage/provider/src/providers/state/historical.rs b/crates/storage/provider/src/providers/state/historical.rs index f0280e4a6a6..fddd4ac3cc9 100644 --- a/crates/storage/provider/src/providers/state/historical.rs +++ b/crates/storage/provider/src/providers/state/historical.rs @@ -23,9 +23,9 @@ use reth_trie::{ trie_cursor::InMemoryTrieCursorFactory, updates::TrieUpdates, witness::TrieWitness, - AccountProof, HashedPostState, HashedPostStateSorted, HashedStorage, KeccakKeyHasher, - MultiProof, MultiProofTargets, StateRoot, StorageMultiProof, StorageRoot, TrieInput, - TrieInputSorted, + AccountProof, ExecutionWitnessMode, HashedPostState, HashedPostStateSorted, HashedStorage, + KeccakKeyHasher, MultiProof, MultiProofTargets, StateRoot, StorageMultiProof, StorageRoot, + TrieInput, TrieInputSorted, }; use reth_trie_db::{ hashed_storage_from_reverts_with_provider, DatabaseProof, DatabaseStateRoot, @@ -524,13 +524,18 @@ impl< }) } - fn witness(&self, input: TrieInput, target: HashedPostState) -> ProviderResult> { + fn witness( + &self, + input: TrieInput, + target: HashedPostState, + mode: ExecutionWitnessMode, + ) -> ProviderResult> { reth_trie_db::with_adapter!(self.provider, |A| { let mut input = input; input.prepend(self.revert_state()?.into()); let nodes_sorted = input.nodes.into_sorted(); let state_sorted = input.state.into_sorted(); - TrieWitness::new( + let witness = TrieWitness::new( InMemoryTrieCursorFactory::new( reth_trie_db::DatabaseTrieCursorFactory::<_, A>::new(self.tx()), &nodes_sorted, @@ -541,10 +546,16 @@ impl< ), ) .with_prefix_sets_mut(input.prefix_sets) - .always_include_root_node() - .compute(target) - .map_err(ProviderError::from) - .map(|hm| hm.into_values().collect()) + .with_execution_witness_mode(mode); + let witness = + if mode.is_canonical() { witness } else { witness.always_include_root_node() }; + witness.compute(target).map_err(ProviderError::from).map(|hm| { + let mut values: Vec<_> = hm.into_values().collect(); + if mode.is_canonical() { + values.sort_unstable(); + } + values + }) }) } } diff --git a/crates/storage/provider/src/providers/state/latest.rs b/crates/storage/provider/src/providers/state/latest.rs index d09d06ec14f..6f029d89c9d 100644 --- a/crates/storage/provider/src/providers/state/latest.rs +++ b/crates/storage/provider/src/providers/state/latest.rs @@ -14,8 +14,9 @@ use reth_trie::{ trie_cursor::InMemoryTrieCursorFactory, updates::TrieUpdates, witness::TrieWitness, - AccountProof, HashedPostState, HashedStorage, KeccakKeyHasher, MultiProof, MultiProofTargets, - StateRoot, StorageMultiProof, StorageRoot, TrieInput, TrieInputSorted, + AccountProof, ExecutionWitnessMode, HashedPostState, HashedStorage, KeccakKeyHasher, + MultiProof, MultiProofTargets, StateRoot, StorageMultiProof, StorageRoot, TrieInput, + TrieInputSorted, }; use reth_trie_db::{DatabaseProof, DatabaseStateRoot, DatabaseStorageProof, DatabaseStorageRoot}; @@ -218,11 +219,16 @@ impl StateProofProvider }) } - fn witness(&self, input: TrieInput, target: HashedPostState) -> ProviderResult> { + fn witness( + &self, + input: TrieInput, + target: HashedPostState, + mode: ExecutionWitnessMode, + ) -> ProviderResult> { reth_trie_db::with_adapter!(self.0, |A| { let nodes_sorted = input.nodes.into_sorted(); let state_sorted = input.state.into_sorted(); - Ok(TrieWitness::new( + let witness = TrieWitness::new( InMemoryTrieCursorFactory::new( reth_trie_db::DatabaseTrieCursorFactory::<_, A>::new(self.tx()), &nodes_sorted, @@ -233,10 +239,14 @@ impl StateProofProvider ), ) .with_prefix_sets_mut(input.prefix_sets) - .always_include_root_node() - .compute(target)? - .into_values() - .collect()) + .with_execution_witness_mode(mode); + let witness = + if mode.is_canonical() { witness } else { witness.always_include_root_node() }; + let mut values: Vec<_> = witness.compute(target)?.into_values().collect(); + if mode.is_canonical() { + values.sort_unstable(); + } + Ok(values) }) } } From 9734e23e2ab54f07b356357c76f70824fd3df2b9 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Sat, 4 Apr 2026 18:45:10 -0300 Subject: [PATCH 3/9] feat(trie): implement ExecutionWitnessMode for configurable witness generation --- crates/trie/trie/src/witness.rs | 77 +++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/crates/trie/trie/src/witness.rs b/crates/trie/trie/src/witness.rs index f5aa2ef0aca..eba50ad68da 100644 --- a/crates/trie/trie/src/witness.rs +++ b/crates/trie/trie/src/witness.rs @@ -15,7 +15,8 @@ use alloy_rlp::{Encodable, EMPTY_STRING_CODE}; use alloy_trie::{nodes::BranchNodeRef, EMPTY_ROOT_HASH}; use reth_execution_errors::{SparseStateTrieErrorKind, StateProofError, TrieWitnessError}; use reth_trie_common::{ - DecodedMultiProofV2, HashedPostState, MultiProofTargetsV2, ProofV2Target, TrieNodeV2, + DecodedMultiProofV2, ExecutionWitnessMode, HashedPostState, MultiProofTargetsV2, ProofV2Target, + TrieNodeV2, }; use reth_trie_sparse::{LeafUpdate, SparseStateTrie, SparseTrie as _}; @@ -33,6 +34,8 @@ pub struct TrieWitness { /// parent state root. /// Set to `false` by default. always_include_root_node: bool, + /// Controls how the witness is generated. + mode: ExecutionWitnessMode, /// Recorded witness. witness: B256Map, } @@ -45,6 +48,7 @@ impl TrieWitness { hashed_cursor_factory, prefix_sets: TriePrefixSetsMut::default(), always_include_root_node: false, + mode: ExecutionWitnessMode::Legacy, witness: HashMap::default(), } } @@ -56,6 +60,7 @@ impl TrieWitness { hashed_cursor_factory: self.hashed_cursor_factory, prefix_sets: self.prefix_sets, always_include_root_node: self.always_include_root_node, + mode: self.mode, witness: self.witness, } } @@ -67,6 +72,7 @@ impl TrieWitness { hashed_cursor_factory, prefix_sets: self.prefix_sets, always_include_root_node: self.always_include_root_node, + mode: self.mode, witness: self.witness, } } @@ -84,6 +90,12 @@ impl TrieWitness { self.always_include_root_node = true; self } + + /// Set the execution witness generation mode. + pub const fn with_execution_witness_mode(mut self, mode: ExecutionWitnessMode) -> Self { + self.mode = mode; + self + } } impl TrieWitness @@ -146,11 +158,11 @@ where sparse_trie.reveal_decoded_multiproof_v2(multiproof)?; // Build storage leaf updates for all accounts with storage changes, split into - // removals and upserts. Removals must be applied first so that branch collapse - // detection fires correctly: if a removal and an insertion target siblings under - // the same branch, processing the removal first may reduce the branch to a single - // blinded child, triggering a proof fetch for the sibling. Processing the insertion - // first would add a new child that keeps the count above one, masking the need. + // removals and upserts. Legacy mode applies removals first to preserve the + // historical witness shape expected by existing consumers: a removal can collapse + // a branch and force proof fetches that some consumers still rely on. Canonical + // mode applies upserts first to avoid those compatibility-only nodes and emit + // the minimized draft-spec witness. let mut storage_removals: B256Map> = B256Map::default(); let mut storage_upserts: B256Map> = B256Map::default(); for (hashed_address, storage) in &state.storages { @@ -169,8 +181,14 @@ where } } - // Apply storage removals first, then upserts, fetching additional proofs as needed. - for storage_updates in [&mut storage_removals, &mut storage_upserts] { + let storage_update_sets = if self.mode.is_canonical() { + [&mut storage_upserts, &mut storage_removals] + } else { + [&mut storage_removals, &mut storage_upserts] + }; + + // Apply storage updates in mode-specific order, fetching additional proofs as needed. + for storage_updates in storage_update_sets { loop { let mut targets = MultiProofTargetsV2::default(); @@ -212,8 +230,10 @@ where } } - // Build account leaf updates, split into removals and upserts (same reasoning - // as for storage updates above). + // Build account leaf updates, split into removals and upserts. Legacy mode keeps + // removals-first for the same compatibility reason as storage updates, while + // canonical mode uses upserts-first so account updates follow the minimized + // draft-spec witness order. let mut account_removals: B256Map = B256Map::default(); let mut account_upserts: B256Map = B256Map::default(); for &hashed_address in state.accounts.keys().chain(state.storages.keys()) { @@ -233,7 +253,12 @@ where if let Some(storage_trie) = sparse_trie.storage_trie_mut(&hashed_address) { storage_trie.root() } else { - self.account_storage_root(hashed_address)? + let record_root_node = !self.mode.is_canonical() || + state + .storages + .get(&hashed_address) + .is_some_and(|storage| !storage.storage.is_empty()); + self.account_storage_root(hashed_address, record_root_node)? }; if account.is_empty() && storage_root == EMPTY_ROOT_HASH { @@ -245,8 +270,14 @@ where } } - // Apply account removals first, then upserts, fetching additional proofs as needed. - for account_updates in [&mut account_removals, &mut account_upserts] { + let account_update_sets = if self.mode.is_canonical() { + [&mut account_upserts, &mut account_removals] + } else { + [&mut account_removals, &mut account_upserts] + }; + + // Apply account updates in mode-specific order, fetching additional proofs as needed. + for account_updates in account_update_sets { loop { let mut targets = MultiProofTargetsV2::default(); @@ -272,6 +303,12 @@ where } } + if self.mode.is_canonical() { + // Empty trie nodes carry no useful witness information and are trivially + // reconstructible from the empty root hash. + self.witness.retain(|_, value| value.as_ref() != [EMPTY_STRING_CODE]); + } + Ok(self.witness) } @@ -306,8 +343,12 @@ where } /// Compute the storage root for an account by walking the storage trie using the cursor - /// factories and trie input prefix sets. Records the root node in the witness. - fn account_storage_root(&mut self, hashed_address: B256) -> Result { + /// factories and trie input prefix sets. Records the root node in the witness when requested. + fn account_storage_root( + &mut self, + hashed_address: B256, + record_root_node: bool, + ) -> Result { let storage_trie_cursor = self .trie_cursor_factory .storage_trie_cursor(hashed_address) @@ -328,8 +369,10 @@ where .compute_root_hash(core::slice::from_ref(&root_node))? .unwrap_or(EMPTY_ROOT_HASH); drop(calculator); - let mut encoded = Vec::new(); - self.record_witness_node(&root_node.node, &mut encoded); + if record_root_node { + let mut encoded = Vec::new(); + self.record_witness_node(&root_node.node, &mut encoded); + } Ok(root_hash) } From 6eb8948cba0b73b5c5c54227e163c5a048c3b274 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Sun, 5 Apr 2026 11:57:43 -0300 Subject: [PATCH 4/9] test(witness): enhance tests for ExecutionWitnessMode with legacy and canonical modes --- crates/trie/db/tests/witness.rs | 231 +++++++++++++++++++++++++++++--- 1 file changed, 213 insertions(+), 18 deletions(-) diff --git a/crates/trie/db/tests/witness.rs b/crates/trie/db/tests/witness.rs index 1e630482f06..6f7cd9a81a8 100644 --- a/crates/trie/db/tests/witness.rs +++ b/crates/trie/db/tests/witness.rs @@ -13,15 +13,18 @@ use reth_primitives_traits::{Account, StorageEntry}; use reth_provider::{test_utils::create_test_provider_factory, HashingWriter}; use reth_storage_api::StorageSettingsCache; use reth_trie::{ - proof::Proof, witness::TrieWitness, HashedPostState, HashedStorage, MultiProofTargets, - StateRoot, + proof::Proof, witness::TrieWitness, ExecutionWitnessMode, HashedPostState, HashedStorage, + LeafNode, MultiProofTargets, Nibbles, StateRoot, StorageRoot, TrieNodeV2, }; use reth_trie_db::{ - DatabaseHashedCursorFactory, DatabaseProof, DatabaseStateRoot, DatabaseTrieCursorFactory, + DatabaseHashedCursorFactory, DatabaseProof, DatabaseStateRoot, DatabaseStorageRoot, + DatabaseTrieCursorFactory, }; type DbStateRoot<'a, TX, A> = StateRoot, DatabaseHashedCursorFactory<&'a TX>>; +type DbStorageRoot<'a, TX, A> = + StorageRoot, DatabaseHashedCursorFactory<&'a TX>>; type DbProof<'a, TX, A> = Proof, DatabaseHashedCursorFactory<&'a TX>>; #[test] @@ -34,20 +37,33 @@ fn includes_empty_node_preimage() { let hashed_slot = B256::random(); reth_trie_db::with_adapter!(provider, |A| { - // witness includes empty state trie root node + let legacy_empty_witness = TrieWitness::new( + DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), + DatabaseHashedCursorFactory::new(provider.tx_ref()), + ) + .with_execution_witness_mode(ExecutionWitnessMode::Legacy) + .compute(HashedPostState { + accounts: HashMap::from_iter([(hashed_address, Some(Account::default()))]), + storages: HashMap::default(), + }) + .unwrap(); assert_eq!( - TrieWitness::new( - DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), - DatabaseHashedCursorFactory::new(provider.tx_ref()), - ) - .compute(HashedPostState { - accounts: HashMap::from_iter([(hashed_address, Some(Account::default()))]), - storages: HashMap::default(), - }) - .unwrap(), + legacy_empty_witness, HashMap::from_iter([(EMPTY_ROOT_HASH, Bytes::from([EMPTY_STRING_CODE]))]) ); + let canonical_empty_witness = TrieWitness::new( + DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), + DatabaseHashedCursorFactory::new(provider.tx_ref()), + ) + .with_execution_witness_mode(ExecutionWitnessMode::Canonical) + .compute(HashedPostState { + accounts: HashMap::from_iter([(hashed_address, Some(Account::default()))]), + storages: HashMap::default(), + }) + .unwrap(); + assert!(canonical_empty_witness.is_empty()); + // Insert account into database provider.insert_account_for_hashing([(address, Some(Account::default()))]).unwrap(); @@ -60,10 +76,11 @@ fn includes_empty_node_preimage() { )])) .unwrap(); - let witness = TrieWitness::new( + let legacy_witness = TrieWitness::new( DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), DatabaseHashedCursorFactory::new(provider.tx_ref()), ) + .with_execution_witness_mode(ExecutionWitnessMode::Legacy) .compute(HashedPostState { accounts: HashMap::from_iter([(hashed_address, Some(Account::default()))]), storages: HashMap::from_iter([( @@ -72,12 +89,30 @@ fn includes_empty_node_preimage() { )]), }) .unwrap(); - assert!(witness.contains_key(&state_root)); + assert!(legacy_witness.contains_key(&state_root)); for node in multiproof.account_subtree.values() { - assert_eq!(witness.get(&keccak256(node)), Some(node)); + assert_eq!(legacy_witness.get(&keccak256(node)), Some(node)); } - // witness includes empty state trie root node - assert_eq!(witness.get(&EMPTY_ROOT_HASH), Some(&Bytes::from([EMPTY_STRING_CODE]))); + assert_eq!(legacy_witness.get(&EMPTY_ROOT_HASH), Some(&Bytes::from([EMPTY_STRING_CODE]))); + + let canonical_witness = TrieWitness::new( + DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), + DatabaseHashedCursorFactory::new(provider.tx_ref()), + ) + .with_execution_witness_mode(ExecutionWitnessMode::Canonical) + .compute(HashedPostState { + accounts: HashMap::from_iter([(hashed_address, Some(Account::default()))]), + storages: HashMap::from_iter([( + hashed_address, + HashedStorage::from_iter(false, [(hashed_slot, U256::from(1))]), + )]), + }) + .unwrap(); + assert!(canonical_witness.contains_key(&state_root)); + for node in multiproof.account_subtree.values() { + assert_eq!(canonical_witness.get(&keccak256(node)), Some(node)); + } + assert!(!canonical_witness.contains_key(&EMPTY_ROOT_HASH)); }); } @@ -185,3 +220,163 @@ fn correctly_decodes_branch_node_values() { } }); } + +#[test] +fn skips_storage_root_node_for_account_only_changes_in_canonical_mode() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + + let address = Address::random(); + let hashed_address = keccak256(address); + let slot = B256::random(); + + provider + .insert_account_for_hashing([( + address, + Some(Account { balance: U256::from(1), ..Default::default() }), + )]) + .unwrap(); + provider + .insert_storage_for_hashing([(address, [StorageEntry { key: slot, value: U256::from(7) }])]) + .unwrap(); + + reth_trie_db::with_adapter!(provider, |A| { + let state_root = DbStateRoot::<_, A>::from_tx(provider.tx_ref()).root().unwrap(); + let storage_root = + DbStorageRoot::<_, A>::from_tx(provider.tx_ref(), address).root().unwrap(); + let proof = as DatabaseProof>::from_tx(provider.tx_ref()); + let multiproof = proof + .multiproof(MultiProofTargets::from_iter([(hashed_address, HashSet::default())])) + .unwrap(); + + let target_state = HashedPostState { + accounts: HashMap::from_iter([( + hashed_address, + Some(Account { balance: U256::from(2), ..Default::default() }), + )]), + storages: HashMap::default(), + }; + + let legacy_witness = TrieWitness::new( + DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), + DatabaseHashedCursorFactory::new(provider.tx_ref()), + ) + .with_execution_witness_mode(ExecutionWitnessMode::Legacy) + .compute(target_state.clone()) + .unwrap(); + assert!(legacy_witness.contains_key(&state_root)); + for node in multiproof.account_subtree.values() { + assert_eq!(legacy_witness.get(&keccak256(node)), Some(node)); + } + assert!(legacy_witness.contains_key(&storage_root)); + + let canonical_witness = TrieWitness::new( + DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), + DatabaseHashedCursorFactory::new(provider.tx_ref()), + ) + .with_execution_witness_mode(ExecutionWitnessMode::Canonical) + .compute(target_state) + .unwrap(); + assert!(canonical_witness.contains_key(&state_root)); + for node in multiproof.account_subtree.values() { + assert_eq!(canonical_witness.get(&keccak256(node)), Some(node)); + } + assert!(!canonical_witness.contains_key(&storage_root)); + }); +} + +#[test] +fn canonical_mode_handles_mixed_storage_inserts_and_removals() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + + let address = Address::random(); + let hashed_address = keccak256(address); + + // Pre-state storage root is a branch with two children at nibbles 1 and 2. + let removed_slot = { + let mut bytes = [0u8; 32]; + bytes[0] = 0x10; + B256::from(bytes) + }; + let retained_slot = { + let mut bytes = [0u8; 32]; + bytes[0] = 0x20; + B256::from(bytes) + }; + let inserted_slot = { + let mut bytes = [0u8; 32]; + bytes[0] = 0x30; + B256::from(bytes) + }; + let retained_leaf = Bytes::from(alloy_rlp::encode(TrieNodeV2::Leaf(LeafNode::new( + Nibbles::unpack(retained_slot).slice(1..), + alloy_rlp::encode_fixed_size(&U256::from(2)).to_vec(), + )))); + let retained_leaf_hash = keccak256(&retained_leaf); + + // The initial proof targets only the removed slot and the missing inserted slot, so the + // surviving sibling starts out blinded. + provider.insert_account_for_hashing([(address, Some(Account::default()))]).unwrap(); + let mut hashed_storage_cursor = + provider.tx_ref().cursor_dup_write::().unwrap(); + hashed_storage_cursor + .upsert(hashed_address, &StorageEntry { key: removed_slot, value: U256::from(1) }) + .unwrap(); + hashed_storage_cursor + .upsert(hashed_address, &StorageEntry { key: retained_slot, value: U256::from(2) }) + .unwrap(); + + reth_trie_db::with_adapter!(provider, |A| { + let state_root = DbStateRoot::<_, A>::from_tx(provider.tx_ref()).root().unwrap(); + let proof = as DatabaseProof>::from_tx(provider.tx_ref()); + let initial_multiproof = proof + .multiproof(MultiProofTargets::from_iter([( + hashed_address, + HashSet::from_iter([removed_slot, inserted_slot]), + )])) + .unwrap(); + assert!(initial_multiproof + .storages + .values() + .flat_map(|storage| storage.subtree.values()) + .all(|node| node != &retained_leaf)); + + // Apply one removal and one insertion in the same storage trie subtree. + let state_diff = HashedPostState { + accounts: HashMap::from_iter([(hashed_address, Some(Account::default()))]), + storages: HashMap::from_iter([( + hashed_address, + HashedStorage::from_iter( + false, + [(removed_slot, U256::ZERO), (inserted_slot, U256::from(3))], + ), + )]), + }; + + // Legacy removes first, so the two-child branch collapses onto the surviving sibling + // before the new child exists. That forces an extra proof fetch for the sibling leaf. + let legacy_witness = TrieWitness::new( + DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), + DatabaseHashedCursorFactory::new(provider.tx_ref()), + ) + .with_execution_witness_mode(ExecutionWitnessMode::Legacy) + .compute(state_diff.clone()) + .unwrap(); + assert!(legacy_witness.contains_key(&state_root)); + assert_eq!(legacy_witness.get(&retained_leaf_hash), Some(&retained_leaf)); + + // Canonical inserts first, so the branch never compresses to a single blinded child and + // the surviving sibling leaf is not needed in the witness. + let canonical_witness = TrieWitness::new( + DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()), + DatabaseHashedCursorFactory::new(provider.tx_ref()), + ) + .with_execution_witness_mode(ExecutionWitnessMode::Canonical) + .compute(state_diff) + .unwrap(); + assert!(canonical_witness.contains_key(&state_root)); + assert!(!canonical_witness.contains_key(&retained_leaf_hash)); + assert!(canonical_witness.iter().all(|(_, node)| node.as_ref() != [EMPTY_STRING_CODE])); + }); +} From 6dac6974b4337822e2ce0dfe6d06fcc6c2b30a64 Mon Sep 17 00:00:00 2001 From: jsign Date: Mon, 6 Apr 2026 09:20:03 -0300 Subject: [PATCH 5/9] fix --- crates/engine/invalid-block-hooks/src/witness.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/engine/invalid-block-hooks/src/witness.rs b/crates/engine/invalid-block-hooks/src/witness.rs index 0a8db16f621..a2261592ced 100644 --- a/crates/engine/invalid-block-hooks/src/witness.rs +++ b/crates/engine/invalid-block-hooks/src/witness.rs @@ -160,7 +160,11 @@ fn generate( hashed_state: reth_trie::HashedPostState, state_provider: Box, ) -> eyre::Result { - let state = state_provider.witness(Default::default(), hashed_state)?; + let state = state_provider.witness( + Default::default(), + hashed_state, + reth_trie::ExecutionWitnessMode::Legacy, + )?; Ok(ExecutionWitness { state, codes: codes.into_values().collect(), From 5d566221747c53a76945c18ade3d3428dd32f758 Mon Sep 17 00:00:00 2001 From: jsign Date: Wed, 8 Apr 2026 10:52:41 -0300 Subject: [PATCH 6/9] feedback Signed-off-by: jsign --- crates/revm/src/witness.rs | 16 +++++++--------- crates/trie/common/src/execution_witness.rs | 20 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/revm/src/witness.rs b/crates/revm/src/witness.rs index e5e05a0e54d..c93137f7b75 100644 --- a/crates/revm/src/witness.rs +++ b/crates/revm/src/witness.rs @@ -47,15 +47,13 @@ impl ExecutionWitnessRecord { ) .collect(), ExecutionWitnessMode::Canonical => { - let mut accessed_codes = B256Map::default(); - for code in statedb.cache.contracts.values() { - let code = code.original_bytes(); - if code.is_empty() { - continue; - } - accessed_codes.entry(keccak256(&code)).or_insert(code); - } - let mut codes: Vec<_> = accessed_codes.into_values().collect(); + let mut codes: Vec<_> = statedb + .cache + .contracts + .values() + .map(|c| c.original_bytes()) + .filter(|code| !code.is_empty()) + .collect(); codes.sort_unstable(); codes } diff --git a/crates/trie/common/src/execution_witness.rs b/crates/trie/common/src/execution_witness.rs index d08df72f8fb..1cc37df98d9 100644 --- a/crates/trie/common/src/execution_witness.rs +++ b/crates/trie/common/src/execution_witness.rs @@ -3,10 +3,26 @@ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum ExecutionWitnessMode { - /// Preserve the current witness shape for compatibility with existing consumers. + /// Produces the legacy execution witness format. #[default] Legacy, - /// Produce the minimized, draft-spec witness form. + /// Produces the canonical spec currently implemented in + /// ethereum/execution-specs@projects/zkevm. The main differences with the + /// legacy format are: + /// - For the `bytecode` field: + /// - It contains only bytecodes required for execution, compared to Legacy which also + /// contains created bytecode. + /// - Compared to Legacy, it does not include empty bytecodes (i.e. 0x80). + /// - Values are sorted lexicographically ascending. + /// - For the `state` field: + /// - Avoids including empty nodes (i.e. 0x80). + /// - Compared to legacy, it does not include storage trie root nodes if no storae is + /// accessed. + /// - It contains the minimum amount of sibilings for post-state root calculation, since it + /// does updates/insertions first and then deletions. Compared to legacy which does the + /// post-state calculation with removals and then insertions/updates, which results in + /// more siblings. + /// - Values are sorted lexicographically ascending. Canonical, } From 6848c130a2247a9508079a06271c0b56a76e2e46 Mon Sep 17 00:00:00 2001 From: jsign Date: Wed, 8 Apr 2026 10:54:15 -0300 Subject: [PATCH 7/9] typo --- crates/trie/common/src/execution_witness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/trie/common/src/execution_witness.rs b/crates/trie/common/src/execution_witness.rs index 1cc37df98d9..717101a7f56 100644 --- a/crates/trie/common/src/execution_witness.rs +++ b/crates/trie/common/src/execution_witness.rs @@ -18,7 +18,7 @@ pub enum ExecutionWitnessMode { /// - Avoids including empty nodes (i.e. 0x80). /// - Compared to legacy, it does not include storage trie root nodes if no storae is /// accessed. - /// - It contains the minimum amount of sibilings for post-state root calculation, since it + /// - It contains the minimum amount of siblings for post-state root calculation, since it /// does updates/insertions first and then deletions. Compared to legacy which does the /// post-state calculation with removals and then insertions/updates, which results in /// more siblings. From 81c4d69a6b87a849fe905dc70b342addf90ba02b Mon Sep 17 00:00:00 2001 From: jsign Date: Wed, 8 Apr 2026 11:43:57 -0300 Subject: [PATCH 8/9] lint --- crates/revm/src/witness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/revm/src/witness.rs b/crates/revm/src/witness.rs index c93137f7b75..8503ed3f52a 100644 --- a/crates/revm/src/witness.rs +++ b/crates/revm/src/witness.rs @@ -1,5 +1,5 @@ use alloc::vec::Vec; -use alloy_primitives::{keccak256, map::B256Map, Bytes, B256}; +use alloy_primitives::{keccak256, Bytes, B256}; use reth_trie::{ExecutionWitnessMode, HashedPostState, HashedStorage}; use revm::database::State; From 484d48042fc89f6d62bb9f3de2707a0c965ae5e5 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 13 Apr 2026 11:39:03 +0200 Subject: [PATCH 9/9] Update crates/trie/common/src/execution_witness.rs Co-authored-by: Brian Picciano --- crates/trie/common/src/execution_witness.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/trie/common/src/execution_witness.rs b/crates/trie/common/src/execution_witness.rs index 717101a7f56..e9c86d87d9c 100644 --- a/crates/trie/common/src/execution_witness.rs +++ b/crates/trie/common/src/execution_witness.rs @@ -16,7 +16,7 @@ pub enum ExecutionWitnessMode { /// - Values are sorted lexicographically ascending. /// - For the `state` field: /// - Avoids including empty nodes (i.e. 0x80). - /// - Compared to legacy, it does not include storage trie root nodes if no storae is + /// - Compared to legacy, it does not include storage trie root nodes if no storage is /// accessed. /// - It contains the minimum amount of siblings for post-state root calculation, since it /// does updates/insertions first and then deletions. Compared to legacy which does the