diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89ce6c9..8b6a0d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,18 @@ jobs: run: cargo clippy --workspace --all-targets --all-features -- -D warnings ef-tests: - name: Ethereum Foundation tests + name: Ethereum Foundation tests (${{ matrix.trie }}) runs-on: ubuntu-latest timeout-minutes: 30 + continue-on-error: ${{ matrix.allow_failure }} + strategy: + fail-fast: false + matrix: + include: + - trie: default + allow_failure: false + - trie: zeth + allow_failure: true env: RUST_LOG: info,sync=error RUST_BACKTRACE: 1 @@ -42,7 +51,7 @@ jobs: with: cache-on-failure: true - name: Run EF tests - run: ./scripts/run_ef_tests.sh + run: ./scripts/run_ef_tests.sh ${{ matrix.trie }} no-std: name: no_std (RISC-V) diff --git a/Cargo.lock b/Cargo.lock index cf45195..238876e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,7 +52,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-serde", - "alloy-trie", + "alloy-trie 0.9.4", "alloy-tx-macros", "arbitrary", "auto_impl", @@ -201,7 +201,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-serde", - "alloy-trie", + "alloy-trie 0.9.4", "borsh", "serde", "serde_with", @@ -594,6 +594,22 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-trie" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec", + "derive_more", + "nybbles 0.3.4", + "serde", + "smallvec", + "tracing", +] + [[package]] name = "alloy-trie" version = "0.9.4" @@ -606,7 +622,7 @@ dependencies = [ "arrayvec", "derive_arbitrary", "derive_more", - "nybbles", + "nybbles 0.4.8", "proptest", "proptest-derive", "serde", @@ -1108,7 +1124,7 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -1241,6 +1257,29 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "bytemuck" version = "1.25.0" @@ -1798,6 +1837,7 @@ dependencies = [ "serde_json", "stateless", "thiserror", + "tries", "walkdir", ] @@ -2902,6 +2942,26 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "nom" version = "7.1.3" @@ -3069,6 +3129,17 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "nybbles" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307" +dependencies = [ + "const-hex", + "serde", + "smallvec", +] + [[package]] name = "nybbles" version = "0.4.8" @@ -3675,6 +3746,26 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3702,6 +3793,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + [[package]] name = "rand" version = "0.8.5" @@ -3861,6 +3961,15 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -3936,7 +4045,7 @@ dependencies = [ "alloy-evm", "alloy-genesis", "alloy-primitives", - "alloy-trie", + "alloy-trie 0.9.4", "auto_impl", "derive_more", "reth-ethereum-forks", @@ -3954,7 +4063,7 @@ dependencies = [ "alloy-eips", "alloy-genesis", "alloy-primitives", - "alloy-trie", + "alloy-trie 0.9.4", "arbitrary", "bytes", "modular-bitfield", @@ -4278,7 +4387,7 @@ dependencies = [ "alloy-evm", "alloy-primitives", "alloy-rlp", - "nybbles", + "nybbles 0.4.8", "reth-storage-errors", "thiserror", ] @@ -4454,7 +4563,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-rpc-types-eth", - "alloy-trie", + "alloy-trie 0.9.4", "arbitrary", "auto_impl", "byteorder", @@ -4680,7 +4789,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", - "alloy-trie", + "alloy-trie 0.9.4", "auto_impl", "itertools 0.14.0", "metrics", @@ -4707,14 +4816,14 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-serde", - "alloy-trie", + "alloy-trie 0.9.4", "arbitrary", "arrayvec", "bytes", "derive_more", "hash-db", "itertools 0.14.0", - "nybbles", + "nybbles 0.4.8", "plain_hasher", "rayon", "reth-codecs", @@ -4751,7 +4860,7 @@ source = "git+https://github.com/paradigmxyz/reth?tag=v1.11.0#564ffa586845fa4a8b dependencies = [ "alloy-primitives", "alloy-rlp", - "alloy-trie", + "alloy-trie 0.9.4", "auto_impl", "rayon", "reth-execution-errors", @@ -4979,6 +5088,36 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.16.1", + "indexmap 2.13.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "rlp" version = "0.5.2" @@ -5385,6 +5524,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.2" @@ -5448,8 +5593,6 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-rpc-types-debug", - "alloy-trie", - "itertools 0.14.0", "reth-chainspec", "reth-consensus", "reth-ethereum-consensus", @@ -5457,13 +5600,13 @@ dependencies = [ "reth-evm", "reth-primitives-traits", "reth-trie-common", - "reth-trie-sparse", "revm-bytecode", "revm-database-interface", "revm-state", "serde", "serde_with", "thiserror", + "tries", ] [[package]] @@ -5671,6 +5814,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -6006,6 +6164,23 @@ dependencies = [ "rlp", ] +[[package]] +name = "tries" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-debug", + "alloy-trie 0.9.4", + "itertools 0.14.0", + "reth-trie-common", + "reth-trie-sparse", + "revm-bytecode", + "revm-database-interface", + "thiserror", + "zeth-mpt", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -6084,6 +6259,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6739,6 +6924,22 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "zeth-mpt" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-trie 0.8.1", + "arrayvec", + "bincode", + "itertools 0.14.0", + "rkyv", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 0eb84f1..30dc563 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,12 @@ repository = "https://github.com/paradigmxyz/stateless" exclude = [".github/"] [workspace] -members = ["crates/stateless", "testing/ef-tests"] +members = [ + "crates/stateless", + "crates/tries", + "crates/zeth-mpt", + "testing/ef-tests", +] resolver = "2" [workspace.lints.rust] @@ -33,6 +38,7 @@ strip = "symbols" [workspace.dependencies] # internal stateless = { path = "crates/stateless" } +tries = { path = "crates/tries" } # reth reth-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.0", default-features = false } @@ -69,10 +75,13 @@ revm-database-interface = { version = "9.0.0", default-features = false } revm-state = { version = "9.0.0", default-features = false } # misc +arrayvec = { version = "0.7", default-features = false } +bincode = "1.3" itertools = { version = "0.14", default-features = false, features = [ "use_alloc", ] } rayon = "1.8" +rkyv = "0.8" serde = { version = "1.0", default-features = false, features = [ "derive", "alloc", diff --git a/README.md b/README.md index 3da5ac8..0f1fb58 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ To run the Ethereum Foundation blockchain tests with stateless validation: ```bash # From the repository root: -./scripts/run_ef_tests.sh +./scripts/run_ef_tests.sh default +./scripts/run_ef_tests.sh zeth ``` Or manually: @@ -56,7 +57,7 @@ Or manually: ```bash # From the repository root: ./scripts/setup_ef_tests.sh -cargo test -p ef-tests --release --features "asm-keccak ef-tests" +EF_TEST_TRIE=default cargo test -p ef-tests --release --features "asm-keccak ef-tests" ``` ## Contributing diff --git a/crates/stateless/Cargo.toml b/crates/stateless/Cargo.toml index 152b6d5..2b17653 100644 --- a/crates/stateless/Cargo.toml +++ b/crates/stateless/Cargo.toml @@ -12,10 +12,12 @@ description = "Stateless Ethereum block validation using execution witnesses" workspace = true [dependencies] +# internal +tries.workspace = true + # alloy alloy-primitives.workspace = true alloy-rlp.workspace = true -alloy-trie.workspace = true alloy-consensus.workspace = true alloy-rpc-types-debug.workspace = true alloy-genesis = { workspace = true, features = ["serde-bincode-compat"] } @@ -26,7 +28,6 @@ reth-primitives-traits.workspace = true reth-ethereum-primitives = { workspace = true, features = ["serde", "serde-bincode-compat"] } reth-evm.workspace = true reth-trie-common.workspace = true -reth-trie-sparse.workspace = true reth-chainspec.workspace = true reth-consensus.workspace = true @@ -37,6 +38,5 @@ revm-state.workspace = true # misc thiserror.workspace = true -itertools.workspace = true serde.workspace = true serde_with.workspace = true diff --git a/crates/stateless/src/error.rs b/crates/stateless/src/error.rs deleted file mode 100644 index 3832e5e..0000000 --- a/crates/stateless/src/error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use alloc::string::String; -use core::fmt; - -/// Error type for witness database operations. -#[derive(Debug)] -pub enum WitnessDbError { - /// Incomplete or missing witness data. - TrieWitness(String), - /// Missing state for a block number. - StateNotFound(u64), - /// RLP decoding error. - Rlp(alloy_rlp::Error), -} - -impl fmt::Display for WitnessDbError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TrieWitness(msg) => write!(f, "trie witness error: {msg}"), - Self::StateNotFound(num) => write!(f, "state for block {num} not found"), - Self::Rlp(err) => write!(f, "RLP decode error: {err}"), - } - } -} - -impl core::error::Error for WitnessDbError {} - -impl revm_database_interface::DBErrorMarker for WitnessDbError {} - -impl From for WitnessDbError { - fn from(err: alloy_rlp::Error) -> Self { - Self::Rlp(err) - } -} diff --git a/crates/stateless/src/lib.rs b/crates/stateless/src/lib.rs index e57aa02..9441413 100644 --- a/crates/stateless/src/lib.rs +++ b/crates/stateless/src/lib.rs @@ -7,7 +7,7 @@ //! # Key Components //! //! * `WitnessDatabase`: An implementation of [`revm_database_interface::Database`] that uses a -//! [`reth_trie_sparse::SparseStateTrie`] populated from witness data, along with provided +//! [`tries::StatelessTrie`] implementation populated from witness data, along with provided //! bytecode and ancestor block hashes, to serve state reads during execution. //! * `stateless_validation`: The core function that orchestrates the stateless validation process. //! It takes a block, its execution witness, ancestor headers, and chain specification, then @@ -35,17 +35,13 @@ extern crate alloc; -/// Error types for witness database operations. -pub mod error; mod recover_block; -/// Sparse trie implementation for stateless validation -pub mod trie; use alloy_genesis::ChainConfig; #[doc(inline)] pub use recover_block::UncompressedPublicKey; #[doc(inline)] -pub use trie::StatelessTrie; +pub use tries::StatelessTrie; #[doc(inline)] pub use validation::stateless_validation; #[doc(inline)] diff --git a/crates/stateless/src/validation.rs b/crates/stateless/src/validation.rs index ca83f06..da58025 100644 --- a/crates/stateless/src/validation.rs +++ b/crates/stateless/src/validation.rs @@ -1,7 +1,6 @@ use crate::{ ExecutionWitness, recover_block::{UncompressedPublicKey, recover_block_with_public_keys}, - trie::{StatelessSparseTrie, StatelessTrie}, witness_db::WitnessDatabase, }; use alloc::{ @@ -24,6 +23,7 @@ use reth_evm::{ }; use reth_primitives_traits::{RecoveredBlock, SealedHeader}; use reth_trie_common::{HashedPostState, KeccakKeyHasher}; +use tries::{StatelessTrie, StatelessTrieError, default::StatelessSparseTrie}; /// BLOCKHASH ancestor lookup window limit per EVM (number of most recent blocks accessible). const BLOCKHASH_ANCESTOR_LIMIT: usize = 256; @@ -106,6 +106,25 @@ pub enum StatelessValidationError { Custom(&'static str), } +impl From for StatelessValidationError { + fn from(err: StatelessTrieError) -> Self { + match err { + StatelessTrieError::WitnessRevealFailed { pre_state_root } => { + Self::WitnessRevealFailed { pre_state_root } + } + StatelessTrieError::StatelessStateRootCalculationFailed => { + Self::StatelessStateRootCalculationFailed + } + StatelessTrieError::StatelessPreStateRootCalculationFailed => { + Self::StatelessPreStateRootCalculationFailed + } + StatelessTrieError::PreStateRootMismatch { got, expected } => { + Self::PreStateRootMismatch { got, expected } + } + } + } +} + /// Performs stateless validation of a block using the provided witness data. pub fn stateless_validation( current_block: Block, diff --git a/crates/stateless/src/witness_db.rs b/crates/stateless/src/witness_db.rs index a8f902a..fa151b4 100644 --- a/crates/stateless/src/witness_db.rs +++ b/crates/stateless/src/witness_db.rs @@ -1,13 +1,12 @@ //! Provides the [`WitnessDatabase`] type, an implementation of [`revm_database_interface::Database`] //! specifically designed for stateless execution environments. -use crate::error::WitnessDbError; -use crate::trie::StatelessTrie; use alloc::{collections::btree_map::BTreeMap, format}; use alloy_primitives::{Address, B256, U256, map::B256Map}; use revm_bytecode::Bytecode; use revm_database_interface::Database; use revm_state::AccountInfo; +use tries::{StatelessTrie, WitnessDbError}; /// An EVM database implementation backed by witness data. /// diff --git a/crates/tries/Cargo.toml b/crates/tries/Cargo.toml new file mode 100644 index 0000000..43753dc --- /dev/null +++ b/crates/tries/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "tries" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "Trie abstractions and implementations for stateless validation" + +[lints] +workspace = true + +[dependencies] +alloy-primitives.workspace = true +alloy-rlp.workspace = true +alloy-rpc-types-debug.workspace = true +alloy-trie.workspace = true +itertools.workspace = true +reth-trie-common.workspace = true +reth-trie-sparse.workspace = true +revm-bytecode.workspace = true +revm-database-interface.workspace = true +thiserror.workspace = true +zeth-mpt = { path = "../zeth-mpt" } diff --git a/crates/stateless/src/trie.rs b/crates/tries/src/default.rs similarity index 83% rename from crates/stateless/src/trie.rs rename to crates/tries/src/default.rs index 0973db9..950ec78 100644 --- a/crates/stateless/src/trie.rs +++ b/crates/tries/src/default.rs @@ -1,5 +1,4 @@ -use crate::error::WitnessDbError; -use crate::validation::StatelessValidationError; +use crate::{StatelessTrie, StatelessTrieError, WitnessDbError}; use alloc::{format, vec::Vec}; use alloy_primitives::{Address, B256, U256, keccak256, map::B256Map}; use alloy_rlp::{Decodable, Encodable}; @@ -14,35 +13,6 @@ use reth_trie_sparse::{ }; use revm_bytecode::Bytecode; -/// Trait for stateless trie implementations that can be used for stateless validation. -pub trait StatelessTrie: core::fmt::Debug { - /// Initialize the stateless trie using the `ExecutionWitness` - fn new( - witness: &ExecutionWitness, - pre_state_root: B256, - ) -> Result<(Self, B256Map), StatelessValidationError> - where - Self: Sized; - - /// Returns the `TrieAccount` that corresponds to the `Address` - /// - /// This method will error if the `ExecutionWitness` is not able to guarantee - /// that the account is missing from the Trie _and_ the witness was complete. - fn account(&self, address: Address) -> Result, WitnessDbError>; - - /// Returns the storage slot value that corresponds to the given (address, slot) tuple. - /// - /// This method will error if the `ExecutionWitness` is not able to guarantee - /// that the storage was missing from the Trie _and_ the witness was complete. - fn storage(&self, address: Address, slot: U256) -> Result; - - /// Computes the new state root from the `HashedPostState`. - fn calculate_state_root( - &mut self, - state: HashedPostState, - ) -> Result; -} - /// `StatelessSparseTrie` structure for usage during stateless validation #[derive(Debug)] pub struct StatelessSparseTrie { @@ -57,7 +27,7 @@ impl StatelessSparseTrie { pub fn new( witness: &ExecutionWitness, pre_state_root: B256, - ) -> Result<(Self, B256Map), StatelessValidationError> { + ) -> Result<(Self, B256Map), StatelessTrieError> { verify_execution_witness(witness, pre_state_root) .map(|(inner, bytecode)| (Self { inner }, bytecode)) } @@ -123,9 +93,9 @@ impl StatelessSparseTrie { pub fn calculate_state_root( &mut self, state: HashedPostState, - ) -> Result { + ) -> Result { calculate_state_root(&mut self.inner, state) - .map_err(|_e| StatelessValidationError::StatelessStateRootCalculationFailed) + .map_err(|_e| StatelessTrieError::StatelessStateRootCalculationFailed) } } @@ -133,7 +103,7 @@ impl StatelessTrie for StatelessSparseTrie { fn new( witness: &ExecutionWitness, pre_state_root: B256, - ) -> Result<(Self, B256Map), StatelessValidationError> { + ) -> Result<(Self, B256Map), StatelessTrieError> { Self::new(witness, pre_state_root) } @@ -145,10 +115,7 @@ impl StatelessTrie for StatelessSparseTrie { self.storage(address, slot) } - fn calculate_state_root( - &mut self, - state: HashedPostState, - ) -> Result { + fn calculate_state_root(&mut self, state: HashedPostState) -> Result { self.calculate_state_root(state) } } @@ -168,13 +135,13 @@ impl StatelessTrie for StatelessSparseTrie { /// contract bytecode, only the hash of it (code hash). /// /// If the roots do not match, it returns an error indicating the witness is invalid -/// for the given `pre_state_root` (see `StatelessValidationError::PreStateRootMismatch`). +/// for the given `pre_state_root` (see [`StatelessTrieError::PreStateRootMismatch`]). // Note: This approach might be inefficient for ZKVMs requiring minimal memory operations, which // would explain why they have for the most part re-implemented this function. fn verify_execution_witness( witness: &ExecutionWitness, pre_state_root: B256, -) -> Result<(SparseStateTrie, B256Map), StatelessValidationError> { +) -> Result<(SparseStateTrie, B256Map), StatelessTrieError> { let provider_factory = DefaultTrieNodeProviderFactory; let mut trie = SparseStateTrie::new(); let mut state_witness = B256Map::default(); @@ -198,17 +165,17 @@ fn verify_execution_witness( // was not inserted into the Trie. It does not mean that the account does not exist. // In order to determine an account not existing, we must do an exclusion proof. trie.reveal_witness(pre_state_root, &state_witness) - .map_err(|_e| StatelessValidationError::WitnessRevealFailed { pre_state_root })?; + .map_err(|_e| StatelessTrieError::WitnessRevealFailed { pre_state_root })?; // Calculate the root let computed_root = trie .root(&provider_factory) - .map_err(|_e| StatelessValidationError::StatelessPreStateRootCalculationFailed)?; + .map_err(|_e| StatelessTrieError::StatelessPreStateRootCalculationFailed)?; if computed_root == pre_state_root { Ok((trie, bytecode)) } else { - Err(StatelessValidationError::PreStateRootMismatch { + Err(StatelessTrieError::PreStateRootMismatch { got: computed_root, expected: pre_state_root, }) diff --git a/crates/tries/src/error.rs b/crates/tries/src/error.rs new file mode 100644 index 0000000..d6f76aa --- /dev/null +++ b/crates/tries/src/error.rs @@ -0,0 +1,46 @@ +use alloc::string::String; +use alloy_primitives::B256; + +/// Errors originating from trie construction and root computation. +#[derive(Debug, thiserror::Error)] +pub enum StatelessTrieError { + /// Error when revealing the witness data failed. + #[error("failed to reveal witness data for pre-state root {pre_state_root}")] + WitnessRevealFailed { + /// The pre-state root used for verification. + pre_state_root: B256, + }, + + /// Error during state root calculation. + #[error("stateless state root calculation failed")] + StatelessStateRootCalculationFailed, + + /// Error calculating the pre-state root from the witness data. + #[error("stateless pre-state root calculation failed")] + StatelessPreStateRootCalculationFailed, + + /// Error when the computed pre-state root does not match the expected one. + #[error("mismatched pre-state root: {got} \n {expected}")] + PreStateRootMismatch { + /// The computed pre-state root. + got: B256, + /// The expected pre-state root. + expected: B256, + }, +} + +/// Error type for witness-backed database operations. +#[derive(Debug, thiserror::Error)] +pub enum WitnessDbError { + /// Incomplete or missing witness data. + #[error("trie witness error: {0}")] + TrieWitness(String), + /// Missing state for a block number. + #[error("state for block {0} not found")] + StateNotFound(u64), + /// RLP decoding error. + #[error("RLP decode error: {0}")] + Rlp(#[from] alloy_rlp::Error), +} + +impl revm_database_interface::DBErrorMarker for WitnessDbError {} diff --git a/crates/tries/src/lib.rs b/crates/tries/src/lib.rs new file mode 100644 index 0000000..30403f9 --- /dev/null +++ b/crates/tries/src/lib.rs @@ -0,0 +1,46 @@ +//! Trie abstractions and implementations for stateless validation. + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", + html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256", + issue_tracker_base_url = "https://github.com/paradigmxyz/stateless/issues/" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![no_std] + +extern crate alloc; + +/// Default trie implementation based on `reth_trie_sparse`. +pub mod default; +mod error; +/// Zeth trie implementation backed by `zeth-mpt`. +pub mod zeth; + +pub use error::{StatelessTrieError, WitnessDbError}; + +use alloy_primitives::{Address, B256, U256, map::B256Map}; +use alloy_rpc_types_debug::ExecutionWitness; +use alloy_trie::TrieAccount; +use reth_trie_common::HashedPostState; +use revm_bytecode::Bytecode; + +/// Trait for trie implementations that can be used for stateless validation. +pub trait StatelessTrie: core::fmt::Debug { + /// Initialize the trie using the [`ExecutionWitness`]. + fn new( + witness: &ExecutionWitness, + pre_state_root: B256, + ) -> Result<(Self, B256Map), StatelessTrieError> + where + Self: Sized; + + /// Returns the [`TrieAccount`] that corresponds to the [`Address`]. + fn account(&self, address: Address) -> Result, WitnessDbError>; + + /// Returns the storage slot value that corresponds to the `(address, slot)` tuple. + fn storage(&self, address: Address, slot: U256) -> Result; + + /// Computes the new state root from the [`HashedPostState`]. + fn calculate_state_root(&mut self, state: HashedPostState) -> Result; +} diff --git a/crates/tries/src/zeth.rs b/crates/tries/src/zeth.rs new file mode 100644 index 0000000..920ae69 --- /dev/null +++ b/crates/tries/src/zeth.rs @@ -0,0 +1,220 @@ +//! Provides a zeth-backed sparse MPT implementation for the stateless validator guest program. +#![allow(warnings)] +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use alloc::vec::Vec; +use core::{cell::RefCell, marker::PhantomData}; + +use crate::{StatelessTrie, StatelessTrieError, WitnessDbError}; +use alloy_primitives::{ + Address, B256, Bytes, KECCAK256_EMPTY, U256, keccak256, + map::{B256Map, hash_map::Entry}, +}; +use alloy_rpc_types_debug::ExecutionWitness; +use alloy_trie::{EMPTY_ROOT_HASH, TrieAccount}; +use reth_trie_common::HashedPostState; +use revm_bytecode::Bytecode; +use zeth_mpt::CachedTrie; + +/// Zero-overhead helper for tries that only contain RLP encoded data. +#[derive(Debug, Clone, Default)] +#[repr(transparent)] +struct RlpTrie { + inner: CachedTrie, + phantom: PhantomData, +} + +impl RlpTrie { + fn new(inner: CachedTrie) -> Self { + Self { inner, phantom: PhantomData } + } + + pub fn from_prehashed( + root: B256, + rlp_by_digest: &B256Map>, + ) -> alloy_rlp::Result { + Ok(Self::new(CachedTrie::from_prehashed_nodes(root, rlp_by_digest)?)) + } + + pub fn get(&self, key: impl AsRef<[u8]>) -> alloy_rlp::Result> { + self.inner.get(key).map(alloy_rlp::decode_exact).transpose() + } + + pub fn insert(&mut self, key: impl AsRef<[u8]>, value: T) { + self.inner.insert(key, alloy_rlp::encode(value)); + } + + pub fn remove(&mut self, key: impl AsRef<[u8]>) -> bool { + self.inner.remove(key) + } + + pub fn hash(&mut self) -> B256 { + self.inner.hash() + } +} + +/// Represents a sparse version of the Ethereum world state. +/// This is significantly more performant than the Reth default. +#[derive(Debug, Clone)] +pub struct SparseState { + /// state MPT containing all used accounts + state: RlpTrie, + /// storage MPTs sorted by the hashed address of their account + storages: RefCell>>, + + /// all relevant MPT nodes by their Keccak hash + rlp_by_digest: B256Map, +} + +impl SparseState { + /// Removes an account from the state. + fn remove_account(&mut self, hashed_address: &B256) { + self.state.remove(hashed_address); + self.storages.get_mut().remove(hashed_address); + } + + /// Clears the storage of an account. + fn clear_storage(&mut self, hashed_address: B256) -> &mut RlpTrie { + match self.storages.get_mut().entry(hashed_address) { + Entry::Occupied(mut entry) => { + entry.insert(RlpTrie::default()); + entry + } + Entry::Vacant(entry) => entry.insert_entry(RlpTrie::default()), + } + .into_mut() + } + + /// Returns a mutable version of the storage trie of the given account. + fn storage_trie_mut(&mut self, hashed_address: B256) -> alloy_rlp::Result<&mut RlpTrie> { + let trie = match self.storages.get_mut().entry(hashed_address) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + // build the storage trie matching the storage root of the account + let storage_root = + self.state.get(hashed_address)?.map_or(EMPTY_ROOT_HASH, |a| a.storage_root); + entry.insert(RlpTrie::from_prehashed(storage_root, &self.rlp_by_digest)?) + } + }; + + Ok(trie) + } +} + +impl StatelessTrie for SparseState { + /// Initialize the stateless trie using the `ExecutionWitness`. + fn new( + witness: &ExecutionWitness, + pre_state_root: B256, + ) -> Result<(Self, B256Map), StatelessTrieError> { + // fist, hash all the RLP nodes once + let rlp_by_digest: B256Map<_> = + witness.state.iter().map(|rlp| (keccak256(rlp), rlp.clone())).collect(); + + // construct the state trie from the witness data and the given state root + let state = RlpTrie::from_prehashed(pre_state_root, &rlp_by_digest) + .map_err(|_| StatelessTrieError::WitnessRevealFailed { pre_state_root })?; + + // hash all the supplied bytecode + let bytecode = witness + .codes + .iter() + .map(|code| (keccak256(code), Bytecode::new_raw(code.clone()))) + .collect(); + + Ok((Self { state, storages: RefCell::new(B256Map::default()), rlp_by_digest }, bytecode)) + } + + /// Returns the `TrieAccount` that corresponds to the `Address`. + fn account(&self, address: Address) -> Result, WitnessDbError> { + let hashed_address = keccak256(address); + match self.state.get(hashed_address)? { + None => Ok(None), + Some(account) => { + // each time an account is accessed, check whether its storage trie already exists + // otherwise construct it from the witness data and the account's storage root + match self.storages.borrow_mut().entry(hashed_address) { + Entry::Vacant(entry) => { + entry.insert(RlpTrie::from_prehashed( + account.storage_root, + &self.rlp_by_digest, + )?); + } + Entry::Occupied(_) => {} + } + + Ok(Some(account)) + } + } + } + + /// Returns the storage slot value that corresponds to the given (address, slot) tuple. + fn storage(&self, address: Address, slot: U256) -> Result { + let storages = self.storages.borrow(); + // storage() is always be called after account(), so the storage trie must already exist + let storage_trie = storages.get(&keccak256(address)).unwrap(); + Ok(storage_trie.get(keccak256(B256::from(slot)))?.unwrap_or(U256::ZERO)) + } + + /// Computes the new state root from the HashedPostState. + fn calculate_state_root(&mut self, state: HashedPostState) -> Result { + let mut removed_accounts = Vec::new(); + for (hashed_address, account) in state.accounts { + // nonexisting accounts must be removed from the state + let Some(account) = account else { + removed_accounts.push(hashed_address); + continue; + }; + + // apply storage changes before computing the storage root + let storage_root = match state.storages.get(&hashed_address) { + None => self.storage_trie_mut(hashed_address).unwrap().hash(), + Some(storage) => { + let storage_trie = if storage.wiped { + self.clear_storage(hashed_address) + } else { + self.storage_trie_mut(hashed_address).unwrap() + }; + + // apply all state modifications + for (hashed_key, value) in &storage.storage { + if !value.is_zero() { + storage_trie.insert(hashed_key, *value); + } + } + // removals must happen last, otherwise unresolved orphans might still exist + for (hashed_key, value) in &storage.storage { + if value.is_zero() { + storage_trie.remove(hashed_key); + } + } + + storage_trie.hash() + } + }; + + // update/insert the account after all changes have been processed + let account = TrieAccount { + nonce: account.nonce, + balance: account.balance, + storage_root, + code_hash: account.bytecode_hash.unwrap_or(KECCAK256_EMPTY), + }; + self.state.insert(hashed_address, account); + } + removed_accounts.iter().for_each(|hashed_address| self.remove_account(hashed_address)); + + Ok(self.state.hash()) + } +} diff --git a/crates/zeth-mpt/Cargo.toml b/crates/zeth-mpt/Cargo.toml new file mode 100644 index 0000000..d614cf5 --- /dev/null +++ b/crates/zeth-mpt/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "zeth-mpt" +edition = "2021" +version.workspace = true +rust-version.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +alloy-trie = { version = "0.8.0", default-features = false, features = ["ethereum"] } +alloy-rlp.workspace = true +alloy-primitives.workspace = true + +arrayvec.workspace = true +bincode = { workspace = true, optional = true } +rkyv = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +itertools = { workspace = true, optional = true } +thiserror = { workspace = true, optional = true } + +[dev-dependencies] +serde_json.workspace = true + +[features] +default = [] +rkyv = ["dep:rkyv", "dep:itertools"] +serde = [ + "dep:serde", + "dep:bincode", + "alloy-primitives/serde", + "alloy-trie/serde", +] +rlp_serialize = ["dep:itertools"] +orphan = ["dep:thiserror"] diff --git a/crates/zeth-mpt/src/lib.rs b/crates/zeth-mpt/src/lib.rs new file mode 100644 index 0000000..6287f3f --- /dev/null +++ b/crates/zeth-mpt/src/lib.rs @@ -0,0 +1,28 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Sparse Merkle Patricia Trie implementation used by the zeth state trie. + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![no_std] + +extern crate alloc; +#[cfg(test)] +extern crate std; + +mod mpt; + +#[cfg(feature = "orphan")] +pub use mpt::orphan; +pub use mpt::{CachedTrie, Trie, EMPTY_ROOT_HASH}; diff --git a/crates/zeth-mpt/src/mpt/children.rs b/crates/zeth-mpt/src/mpt/children.rs new file mode 100644 index 0000000..9796f52 --- /dev/null +++ b/crates/zeth-mpt/src/mpt/children.rs @@ -0,0 +1,195 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + memoize::Memoization, + node::{Child, Node}, +}; +use alloc::boxed::Box; +use core::slice::Iter; + +/// Implements a helper wrapper for the children of a Branch node. +/// +/// This wrapper offers various convenience features and assures that there is never a Null child. +#[derive(Debug, Clone)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(transparent), + serde(bound(serialize = "Node: serde::Serialize")), + serde(bound(deserialize = "Node: serde::Deserialize<'de>")) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize), + rkyv(bytecheck(bounds(__C: rkyv::validation::ArchiveContext, __C::Error: rkyv::rancor::Source))), + rkyv(serialize_bounds(__S: rkyv::ser::Writer + rkyv::ser::Allocator, __S::Error: rkyv::rancor::Source)), + rkyv(deserialize_bounds(__D::Error: rkyv::rancor::Source, M: Default)) +)] +pub(super) struct Children( + #[cfg_attr(feature = "rkyv", rkyv(omit_bounds))] [Option>>; 16], +); + +impl Default for Children { + fn default() -> Self { + Self(Default::default()) + } +} + +impl PartialEq for Children { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for Children where Node: Eq {} + +/// A view into a single entry in a children map, which may either be vacant or occupied. +/// +/// This `enum` is constructed from the [`Children::entry`] method. +pub(super) enum Entry<'a, M> { + Vacant(VacantEntry<'a, M>), + Occupied(OccupiedEntry<'a, M>), +} + +/// A view into a vacant entry in a children map. +/// It is part of the [`Entry`] enum. +pub(super) struct VacantEntry<'a, M> { + child: &'a mut Option>, +} + +/// A view into an occupied entry in a children map. +/// It is part of the [`Entry`] enum. +pub(super) struct OccupiedEntry<'a, M> { + child: &'a mut Option>, +} + +impl Drop for OccupiedEntry<'_, M> { + fn drop(&mut self) { + if matches!(self.get(), Node::Null) { + *self.child = None; + } + } +} + +impl<'a, M> Entry<'a, M> { + #[inline] + const fn new(child: &'a mut Option>) -> Self { + match child { + None => Entry::Vacant(VacantEntry { child }), + Some(_) => Entry::Occupied(OccupiedEntry { child }), + } + } +} + +impl VacantEntry<'_, M> { + /// Sets the child of the entry with the `VacantEntry`'s index, and returns a mutable reference + /// to it. + #[inline] + pub(super) fn insert(self, child: Child) { + assert!(!matches!(child.as_ref(), Node::Null)); + *self.child = Some(child) + } +} + +impl OccupiedEntry<'_, M> { + /// Gets a reference to the child node in the entry. + #[inline] + pub(super) fn get(&self) -> &Node { + // SAFETY: an OccupiedEntry is only created for a child that is not `None` + unsafe { self.child.as_deref().unwrap_unchecked() } + } + + /// Gets a mutable reference to the child node in the entry. + #[inline] + pub(super) fn get_mut(&mut self) -> &mut Node { + // SAFETY: an OccupiedEntry is only created for a child that is not `None` + unsafe { self.child.as_deref_mut().unwrap_unchecked() } + } +} + +#[allow(dead_code)] +impl Children { + #[inline] + pub(super) fn get(&self, idx: u8) -> Option<&Node> { + self.0[idx as usize].as_deref() + } + + #[inline] + pub(super) unsafe fn get_unchecked(&self, idx: u8) -> Option<&Node> { + self.0.get_unchecked(idx as usize).as_deref() + } + + #[inline] + pub(super) const fn entry(&mut self, idx: u8) -> Entry<'_, M> { + Entry::new(&mut self.0[idx as usize]) + } + + #[inline] + pub(super) fn insert(&mut self, idx: u8, child: Child) { + assert!(!matches!(child.as_ref(), Node::Null)); + self.0[idx as usize] = Some(child); + } + + #[inline] + pub(super) fn len(&self) -> usize { + self.0.iter().flatten().count() + } + + pub(super) fn take_single_child(&mut self) -> Option<(u8, Child)> { + let mut child_idx = None; + for (i, child) in self.0.iter().enumerate() { + if child.is_some() { + if child_idx.is_some() { + return None; // more than one child found + } + child_idx = Some(i); + } + } + // SAFETY: if `child_idx` is only set when the corresponding child is `Some` + child_idx.map(|i| (i as u8, unsafe { self.0[i].take().unwrap_unchecked() })) + } + + #[inline] + pub(super) fn iter(&self) -> Iter<'_, Option>> { + self.0.iter() + } + + #[inline] + pub(super) fn into_iter(self) -> impl Iterator>> { + self.0.into_iter() + } + + #[inline] + pub(super) fn entries(&mut self) -> impl Iterator> { + self.0.iter_mut().map(Entry::new) + } +} + +impl Children { + #[inline] + pub(super) fn memoize(&mut self) { + self.0.iter_mut().flatten().for_each(|child| child.memoize()) + } +} + +impl>, const N: usize> From<[(u8, C); N]> for Children { + fn from(arr: [(u8, C); N]) -> Self { + let mut children = Children::default(); + for (idx, child) in arr { + children.insert(idx, child.into()); + } + children + } +} diff --git a/crates/zeth-mpt/src/mpt/memoize.rs b/crates/zeth-mpt/src/mpt/memoize.rs new file mode 100644 index 0000000..dd45089 --- /dev/null +++ b/crates/zeth-mpt/src/mpt/memoize.rs @@ -0,0 +1,53 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::rlp::RlpNode; + +pub(super) trait Memoization: Default { + fn clear(&mut self); + fn get(&self) -> Option<&RlpNode>; + fn set(&mut self, rlp_node: RlpNode); +} + +#[derive(Debug, Clone, Copy, Default)] +pub(super) struct NoCache; + +impl Memoization for NoCache { + #[inline] + fn clear(&mut self) {} + #[inline] + fn get(&self) -> Option<&RlpNode> { + None + } + #[inline] + fn set(&mut self, _: RlpNode) {} +} + +#[derive(Debug, Clone, Default)] +pub(super) struct Cache(Option); + +impl Memoization for Cache { + #[inline] + fn clear(&mut self) { + self.0 = None + } + #[inline] + fn get(&self) -> Option<&RlpNode> { + self.0.as_ref() + } + #[inline] + fn set(&mut self, rlp_node: RlpNode) { + self.0 = Some(rlp_node) + } +} diff --git a/crates/zeth-mpt/src/mpt/mod.rs b/crates/zeth-mpt/src/mpt/mod.rs new file mode 100644 index 0000000..20ddb7f --- /dev/null +++ b/crates/zeth-mpt/src/mpt/mod.rs @@ -0,0 +1,799 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A sparse Merkle Patricia trie implementation. + +use alloc::vec::Vec; +use alloy_primitives::{keccak256, map::B256Map, Bytes, B256}; +use alloy_trie::Nibbles; +use children::Children; +use core::{cmp::PartialEq, fmt::Debug}; +use memoize::{Cache, NoCache}; +use nibbles::NibbleSlice; +use node::Node; + +mod children; + +mod memoize; +mod nibbles; +mod node; +#[cfg(feature = "orphan")] +pub mod orphan; +#[cfg(feature = "rkyv")] +mod rkyv; +mod rlp; +#[cfg(feature = "serde")] +mod serde; + +pub use alloy_trie::EMPTY_ROOT_HASH; + +/// A sparse Merkle Patricia trie storing byte values. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "rkyv", derive(::rkyv::Archive, ::rkyv::Serialize, ::rkyv::Deserialize))] +pub struct Trie(Node); + +impl Trie { + /// Retrieves the value associated with a given key. + /// + /// # Panics + /// + /// It panics when neither inclusion nor exclusion of the key can be guaranteed. + #[inline] + pub fn get(&self, key: impl AsRef<[u8]>) -> Option<&[u8]> { + self.0.get(NibbleSlice::from(&Nibbles::unpack(key))).map(|b| b.as_ref()) + } + + /// Inserts a key-value pair into the trie. + /// + /// # Panics + /// + /// This method may panic under the following conditions: + /// + /// * If the insertion would result in a value being stored directly in a branch node, which is + /// not allowed in this trie implementation. + /// * If the key to be inserted corresponds to a part of the trie that has not been resolved + /// (i.e., the node is represented by a digest and the full node is not available). + #[inline] + pub fn insert(&mut self, key: impl AsRef<[u8]>, value: impl Into) { + self.0.insert(NibbleSlice::from(&Nibbles::unpack(key)), value.into()); + } + + /// Removes a key-value pair from the trie. + /// + /// If the key exists in the trie, it is removed along with its associated value, and the method + /// returns `true`. If the key does not exist, the trie remains unchanged, and the method + /// returns `false`. + /// + /// # Panics + /// + /// This method may panic under the following conditions: + /// + /// * When neither the inclusion nor exclusion of the key can be guaranteed. + /// * If the removal of the key leads to a branch node having only a single, non-resolved child. + /// In such cases, the correct pruning of the trie cannot be guaranteed, indicating a + /// potential issue with the trie's construction. + #[inline] + pub fn remove(&mut self, key: impl AsRef<[u8]>) -> bool { + self.0.remove(NibbleSlice::from(&Nibbles::unpack(key))) + } + + /// Returns the number of full nodes in the trie. + /// + /// A full node is a node that needs to be fully encoded to compute the root hash. + #[inline] + pub fn size(&self) -> usize { + self.0.size() + } + + /// Computes and returns the hash of the trie's root node. + #[inline] + pub fn hash_slow(&self) -> B256 { + self.0.hash() + } + + /// Clears the trie, removing all key-value pairs. + #[inline] + pub fn clear(&mut self) { + self.0 = Node::Null + } + + /// Resolves currently unresolved nodes within the trie using the provided RLP-encoded nodes. + /// + /// This method iterates through the provided RLP-encoded nodes, computes the Keccak-256 hash of + /// each node, and attempts to replace any internal `Node::Digest` entries matching that hash + /// with the decoded node. + /// + /// # Errors + /// + /// This function returns an error if it encounters any issues during the decoding of RLP + /// encoded nodes or if the provided nodes result in an invalid trie structure. + pub fn hydrate_from_rlp>( + &mut self, + nodes: impl IntoIterator, + ) -> alloy_rlp::Result<()> { + let rlp_by_digest = nodes.into_iter().map(|rlp| (keccak256(&rlp), rlp)).collect(); + self.0.resolve_digests(&rlp_by_digest) + } + + /// Converts the trie into a [CachedTrie]. + pub fn into_cached(self) -> CachedTrie { + fn rec(root: Node) -> Node { + match root { + Node::Null => Node::Null, + Node::Leaf(prefix, value, _) => Node::Leaf(prefix, value, Cache::default()), + Node::Extension(prefix, child, _) => { + Node::Extension(prefix, rec(*child).into(), Cache::default()) + } + Node::Branch(children, _) => { + let mut cached_children = Children::default(); + for (i, child) in children.into_iter().enumerate() { + if let Some(child) = child { + cached_children.insert(i as u8, rec(*child).into()); + } + } + Node::Branch(cached_children, Cache::default()) + } + Node::Digest(digest) => Node::Digest(digest), + } + } + + CachedTrie { inner: rec(self.0), hash: None } + } + + /// Returns the RLP-encoded nodes of the trie in preorder. It may return duplicate nodes. + /// + /// Each value but the first, represents a node with RLP-length >= 32, while shorter nodes are + /// included inline. + #[inline] + pub fn rlp_nodes(&self) -> Vec { + self.0.rlp_nodes() + } + + /// Creates a new trie that only contains a digest of the root. + #[inline] + pub const fn from_digest(digest: B256) -> Self { + Self(Node::Digest(digest)) + } + + /// Creates a new trie from the given RLP encoded nodes. + /// + /// The first node provided must always be the root node. The remaining nodes can be in any + /// order and are resolved if they are referenced (directly or indirectly) by the root node. + /// + /// Nodes that are referenced by the root node (either directly or indirectly) but are not + /// provided in the input are represented by their hash digests within the trie. This allows for + /// the computation of the root hash and ensures that it matches the root hash of the fully + /// resolved trie, even if some nodes are missing. + /// + /// # Errors + /// + /// This function returns an error if it encounters any issues during the decoding of RLP + /// encoded nodes or if the provided nodes result in an invalid trie structure. + #[inline] + pub fn from_rlp>(nodes: impl IntoIterator) -> alloy_rlp::Result { + Ok(Self(Node::from_rlp(nodes)?)) + } + + /// Creates a new trie from a root digest and a map of pre-hashed, RLP-encoded nodes. + /// + /// This method offers an efficient way to construct a trie when the node digests are already + /// known, as it avoids re-computing the hashes. + /// + /// It is crucial that the provided `rlp_by_digest` map contains keys that are the correct + /// `keccak256` hashes of their corresponding RLP-encoded values. If the hashes are incorrect, + /// the resulting trie will be invalid, potentially leading to a different root hash than + /// the one provided and subsequent logical errors. + #[inline] + pub fn from_prehashed_nodes( + root: B256, + rlp_by_digest: &B256Map>, + ) -> alloy_rlp::Result { + let mut trie = Self::from_digest(root); + trie.0.resolve_digests(rlp_by_digest)?; + Ok(trie) + } +} + +impl> FromIterator<(K, Bytes)> for Trie { + fn from_iter>(iter: T) -> Self { + let mut trie = Self::default(); + iter.into_iter().for_each(|(k, v)| trie.insert(k, v)); + + trie + } +} + +/// A caching version of a sparse Merkle Patricia trie that stores byte values. +/// +/// `CachedTrie` enhances the basic `Trie` structure by caching the hash of each node. This +/// optimization significantly reduces the computational cost associated with operations that +/// require multiple hash calculations after small trie modifications. +/// +/// It maintains the same interface as `Trie`, allowing for seamless integration into existing +/// systems that rely on the non-caching version. The internal caching mechanism is transparent to +/// the user, automatically updating cached hashes as the trie is modified. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "rkyv", derive(::rkyv::Archive, ::rkyv::Serialize, ::rkyv::Deserialize))] +pub struct CachedTrie { + #[cfg_attr( + all(feature = "serde", feature = "rlp_serialize"), + serde(with = "serde::rlp_nodes") + )] + #[cfg_attr(all(feature = "rkyv", feature = "rlp_serialize"), rkyv(with = rkyv::RlpNodes))] + inner: Node, + #[cfg_attr(feature = "serde", serde(skip))] + #[cfg_attr(feature = "rkyv", rkyv(with = ::rkyv::with::Skip))] + hash: Option, +} + +impl Default for CachedTrie { + #[inline] + fn default() -> Self { + Self { inner: Node::Null, hash: Some(EMPTY_ROOT_HASH) } + } +} + +impl CachedTrie { + /// Retrieves the value associated with a given key. + /// + /// See [`Trie::get`] for detailed documentation. + #[inline] + pub fn get(&self, key: impl AsRef<[u8]>) -> Option<&[u8]> { + self.inner.get(NibbleSlice::from(&Nibbles::unpack(key))).map(|b| b.as_ref()) + } + + /// Inserts a key-value pair into the trie. + /// + /// See [`Trie::insert`] for detailed documentation. + #[inline] + pub fn insert(&mut self, key: impl AsRef<[u8]>, value: impl Into) { + self.inner.insert(NibbleSlice::from(&Nibbles::unpack(key)), value.into()); + self.hash = None; + } + + /// Removes a key-value pair from the trie. + /// + /// See [`Trie::remove`] for detailed documentation. + #[inline] + pub fn remove(&mut self, key: impl AsRef<[u8]>) -> bool { + if !self.inner.remove(NibbleSlice::from(&Nibbles::unpack(key))) { + return false; + } + self.hash = None; + true + } + + /// Returns the number of full nodes in the trie. + /// + /// See [`Trie::size`] for detailed documentation. + #[inline] + pub fn size(&self) -> usize { + self.inner.size() + } + + /// Computes and returns the hash of the trie's root node. + /// + /// This method may utilize cached hashes within the internal node structure (if available) to + /// speed up the computation. However, it does not update the cached root hash of this + /// `CachedTrie` instance. For optimal performance with `CachedTrie`, prefer the [`Self::hash`] + /// method, which manages the root hash cache. This method mirrors [`Trie::hash_slow`], and is + /// similarly less efficient. + #[inline] + pub fn hash_slow(&self) -> B256 { + match self.hash { + None => self.inner.hash(), + Some(hash) => hash, + } + } + + /// Computes and returns the hash of the trie's root node. + /// + /// If the root hash is already cached, it is returned directly. Otherwise, the hash is + /// computed, cached, and then returned. This method also triggers an internal `memoize` + /// operation on the underlying `Node` structure, populating its cache with the hashes of its + /// sub-nodes, further optimizing future hash computations. + /// + /// This is the preferred method for obtaining the root hash of a `CachedTrie` as it leverages + /// and updates the cache for optimal performance. + #[inline] + pub fn hash(&mut self) -> B256 { + *self.hash.get_or_insert_with(|| { + self.inner.memoize(); + self.inner.hash() + }) + } + + /// Clears the trie, removing all key-value pairs. + #[inline] + pub fn clear(&mut self) { + *self = Self { inner: Node::Null, hash: Some(EMPTY_ROOT_HASH) } + } + + /// Returns whether the hash is currently cached or needs to be recomputed. + #[inline] + pub const fn is_cached(&self) -> bool { + self.hash.is_some() + } + + /// Resolves currently unresolved nodes within the trie using the provided RLP-encoded nodes. + /// + /// See [`Trie::hydrate_from_rlp`] for detailed documentation. + #[inline] + pub fn hydrate_from_rlp>( + &mut self, + nodes: impl IntoIterator, + ) -> alloy_rlp::Result<()> { + let rlp_by_digest = nodes.into_iter().map(|rlp| (keccak256(&rlp), rlp)).collect(); + self.inner.resolve_digests(&rlp_by_digest) + } + + /// Returns the RLP-encoded nodes of the trie in preorder. + /// + /// See [`Trie::rlp_nodes`] for detailed documentation. + #[inline] + pub fn rlp_nodes(&self) -> Vec { + self.inner.rlp_nodes() + } + + /// Creates a new trie that only contains a digest of the root. + #[inline] + pub fn from_digest(digest: B256) -> Self { + if digest == EMPTY_ROOT_HASH { + Self::default() + } else { + Self { inner: Node::Digest(digest), hash: Some(digest) } + } + } + + /// Creates a new trie from the given RLP encoded nodes. + /// + /// See [`Trie::from_rlp`] for detailed documentation. + #[inline] + pub fn from_rlp>(nodes: impl IntoIterator) -> alloy_rlp::Result { + let root = Node::from_rlp(nodes)?; + + Ok(Self { inner: root, hash: None }) + } + + /// Creates a new trie from a root digest and a map of pre-hashed, RLP-encoded nodes. + /// + /// See [`Trie::from_prehashed_nodes`] for detailed documentation. + #[inline] + pub fn from_prehashed_nodes( + root: B256, + rlp_by_digest: &B256Map>, + ) -> alloy_rlp::Result { + let mut trie = Self::from_digest(root); + trie.inner.resolve_digests(rlp_by_digest)?; + Ok(trie) + } +} + +impl PartialEq for CachedTrie { + /// Equality between cached tries ignores the cache. + #[inline] + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for CachedTrie {} + +impl> FromIterator<(K, Bytes)> for CachedTrie { + fn from_iter>(iter: T) -> Self { + let mut trie = Self::default(); + iter.into_iter().for_each(|(k, v)| trie.insert(k, v)); + + trie + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::{collections::BTreeMap, vec, vec::Vec}; + use alloy_primitives::{b256, keccak256, Bytes, U256}; + use alloy_trie::HashBuilder; + use children::Children; + use core::borrow::Borrow; + + const N: usize = 512; + + fn trie_root(iter: impl IntoIterator>) -> B256 + where + K: AsRef<[u8]>, + V: AsRef<[u8]>, + { + let mut hb = HashBuilder::default(); + + let mut sorted_data: Vec<_> = iter.into_iter().collect(); + sorted_data.sort_by(|a, b| a.borrow().0.as_ref().cmp(b.borrow().0.as_ref())); + for (key, val) in sorted_data.iter().map(Borrow::borrow) { + hb.add_leaf(Nibbles::unpack(key), val.as_ref()); + } + + hb.root() + } + + #[test] + fn empty_root_hash() { + assert_eq!(EMPTY_ROOT_HASH, keccak256(vec![alloy_rlp::EMPTY_STRING_CODE])); + } + + #[test] + fn mpt_null() { + let trie = Trie(Node::Null); + assert_eq!(trie, Trie::from_rlp(trie.0.rlp_nodes()).unwrap()); + + assert_eq!(trie.hash_slow(), EMPTY_ROOT_HASH); + assert_eq!(trie.size(), 0); + + // the empty trie provides a non-inclusion proof for any key + assert_eq!(trie.get([]), None); + assert_eq!(trie.get([0]), None); + assert_eq!(trie.get([1, 2, 3]), None); + } + + #[test] + fn mpt_digest() { + let trie = Trie::from_digest(B256::ZERO); + assert_eq!(trie, Trie::from_rlp(trie.0.rlp_nodes()).unwrap()); + + assert_eq!(trie.hash_slow(), B256::ZERO); + assert_eq!(trie.size(), 0); + } + + #[test] + fn mpt_leaf() { + let trie = Trie(Node::Leaf(Nibbles::unpack(B256::ZERO), vec![0].into(), NoCache)); + assert_eq!(trie, Trie::from_rlp(trie.0.rlp_nodes()).unwrap()); + + // a leave counts as a full node + assert_eq!(trie.size(), 1); + + // a single leave proves the inclusion of the key and non-inclusion of any other key + assert_eq!(trie.get(B256::ZERO), Some(&[0][..])); + assert_eq!(trie.get([]), None); + assert_eq!(trie.get([0]), None); + assert_eq!(trie.get([1, 2, 3]), None); + } + + #[test] + fn mpt_extension() { + let child = Node::Branch( + Children::from([ + (0, Node::Leaf(Nibbles::from_nibbles([0; 62]), vec![0].into(), NoCache)), + (1, Node::Leaf(Nibbles::from_nibbles([1; 62]), vec![1].into(), NoCache)), + ]), + NoCache, + ); + let trie = Trie(Node::Extension(Nibbles::from_nibbles([0; 1]), child.into(), NoCache)); + assert_eq!(trie, Trie::from_rlp(trie.0.rlp_nodes()).unwrap()); + + // there are one branch, two leaves plus one extension + assert_eq!(trie.size(), 4); + + assert_eq!(trie.get(B256::ZERO), Some(&[0][..])); + assert_eq!( + trie.get(b256!("0111111111111111111111111111111111111111111111111111111111111111")), + Some(&[1][..]) + ); + assert_eq!(trie.get([]), None); + assert_eq!(trie.get([0]), None); + assert_eq!(trie.get([1, 2, 3]), None); + assert_eq!(trie.get(B256::repeat_byte(0x11)), None); + } + + #[test] + fn mpt_branch() { + let trie = Trie(Node::Branch( + Children::from([ + (0, Node::Leaf(Nibbles::from_nibbles([0; 63]), vec![0].into(), NoCache)), + (1, Node::Leaf(Nibbles::from_nibbles([1; 63]), vec![1].into(), NoCache)), + ]), + NoCache, + )); + assert_eq!(trie, Trie::from_rlp(trie.0.rlp_nodes()).unwrap()); + + // there are one branch plus two leaves + assert_eq!(trie.size(), 3); + + assert_eq!(trie.get(B256::repeat_byte(0x00)), Some(&[0][..])); + assert_eq!(trie.get(B256::repeat_byte(0x11)), Some(&[1][..])); + assert_eq!(trie.get([]), None); + assert_eq!(trie.get([0]), None); + assert_eq!(trie.get([1, 2, 3]), None); + } + + #[test] + fn short_encoding() { + // 4 leaves with 1-byte long keys, the resulting root node should be shorter than 32 bytes + let trie = Trie(Node::Branch( + Children::from([ + (0, Node::Leaf(Nibbles::from_nibbles([0]), vec![0].into(), NoCache)), + (1, Node::Leaf(Nibbles::from_nibbles([1]), vec![0].into(), NoCache)), + (2, Node::Leaf(Nibbles::from_nibbles([2]), vec![0].into(), NoCache)), + (3, Node::Leaf(Nibbles::from_nibbles([3]), vec![0].into(), NoCache)), + ]), + NoCache, + )); + assert!(trie.0.rlp_encoded().len() < 32); + let rlp = trie.0.rlp_nodes(); + + assert_eq!(trie, Trie::from_rlp(&rlp).unwrap()); + assert_eq!( + trie.hash_slow(), + trie_root([([0x00], vec![0]), ([0x11], vec![0]), ([0x22], vec![0]), ([0x33], vec![0])]) + ); + assert_eq!(trie.hash_slow(), CachedTrie::from_rlp(&rlp).unwrap().hash(),); + } + + #[test] + fn b256_encoding() { + // 2 leaves with 5-byte long keys, the resulting root node should be exactly 32 bytes + let trie = Trie(Node::Branch( + Children::from([ + (0, Node::Leaf(Nibbles::from_nibbles([0]), vec![0, 1, 2, 3, 4].into(), NoCache)), + (1, Node::Leaf(Nibbles::from_nibbles([1]), vec![0, 1, 2, 3, 4].into(), NoCache)), + ]), + NoCache, + )); + assert_eq!(trie.0.rlp_encoded().len(), 32); + let rlp = trie.0.rlp_nodes(); + + assert_eq!(trie, Trie::from_rlp(&rlp).unwrap()); + assert_eq!( + trie.hash_slow(), + trie_root([([0x00], vec![0, 1, 2, 3, 4]), ([0x11], vec![0, 1, 2, 3, 4]),]) + ); + assert_eq!(trie.hash_slow(), CachedTrie::from_rlp(&rlp).unwrap().hash(),); + } + + #[test] + #[should_panic] + fn get_digest() { + let trie = Trie(Node::Digest(B256::ZERO)); + trie.get([]); + } + + #[test] + fn insert_empty_key() { + let mut trie = Trie::default(); + + trie.insert([], b"empty".to_vec()); + assert_eq!(trie.get([]), Some(b"empty".as_ref())); + assert!(trie.remove([])); + } + + #[test] + fn insert() { + let leaves = vec![ + ("painting", "place"), + ("guest", "ship"), + ("mud", "leave"), + ("paper", "call"), + ("gate", "boast"), + ("tongue", "gain"), + ("baseball", "wait"), + ("tale", "lie"), + ("mood", "cope"), + ("menu", "fear"), + ]; + + let mut trie = Trie::default(); + for (key, value) in &leaves { + trie.insert(key, value.as_bytes()); + } + + for (key, value) in &leaves { + assert_eq!(trie.get(key), Some(value.as_bytes())); + } + assert_eq!(trie.hash_slow(), trie_root(&leaves)); + } + + #[test] + fn index_trie() { + let leaves: Vec<(Vec, Bytes)> = (0..N) + .map(|i| { + let rlp = alloy_rlp::encode(i); + (rlp.clone(), rlp.into()) + }) + .collect(); + + // insert + let mut trie = Trie::default(); + for (i, (key, value)) in leaves.iter().enumerate() { + trie.insert(key, value.clone()); + + // check hash against trie build in reverse + let mut reference = Trie::default(); + for (k, v) in leaves.iter().take(i + 1).rev() { + reference.insert(k, v.clone()); + } + assert_eq!(trie, reference); + } + + assert_eq!(trie.hash_slow(), trie_root(&leaves)); + + // delete + for (i, (key, _)) in leaves.iter().enumerate() { + assert!(trie.remove(key)); + + let mut reference = Trie::default(); + for (k, v) in leaves.iter().rev().take(N - 1 - i) { + reference.insert(k, v.clone()); + } + assert_eq!(trie, reference); + } + + assert_eq!(trie.hash_slow(), EMPTY_ROOT_HASH); + } + + #[test] + fn keccak_trie() { + let leaves: Vec<(B256, Bytes)> = + (0..N).map(|i| (keccak256(i.to_be_bytes()), alloy_rlp::encode(i).into())).collect(); + + // insert + let mut trie = Trie::default(); + for (i, (key, value)) in leaves.iter().enumerate() { + trie.insert(key, value.clone()); + + // check hash against trie build in reverse + let mut reference = Trie::default(); + for (k, v) in leaves.iter().take(i + 1).rev() { + reference.insert(k, v.clone()); + } + assert_eq!(trie, reference); + } + + assert_eq!(trie.hash_slow(), trie_root(&leaves)); + + // delete + for (i, (key, _)) in leaves.iter().enumerate() { + assert!(trie.remove(key)); + + let mut reference = Trie::default(); + for (k, v) in leaves.iter().rev().take(N - 1 - i) { + reference.insert(k, v.clone()); + } + assert_eq!(trie, reference); + } + + assert_eq!(trie.hash_slow(), EMPTY_ROOT_HASH); + } + + #[test] + fn hash_sparse_mpt() { + let leaves: BTreeMap<_, _> = (0..N) + .map(|i| { + let key = U256::from(i); + (Nibbles::unpack(keccak256(B256::from(key))), alloy_rlp::encode(key)) + }) + .collect(); + + // generate proofs only for every other leaf + let proof_keys = leaves.keys().step_by(2).cloned().collect(); + let mut hb = HashBuilder::default().with_proof_retainer(proof_keys); + leaves.into_iter().for_each(|(k, v)| hb.add_leaf(k, &v)); + let exp_hash = hb.root(); + + // reconstruct the trie from the RLP encoded proofs and verify the root hash + let mpt = Trie::from_rlp( + hb.take_proof_nodes().into_nodes_sorted().into_iter().map(|node| node.1), + ) + .unwrap(); + assert!(mpt.size() < N); + assert_eq!(mpt.hash_slow(), exp_hash); + } + + #[test] + fn parse_empty_proof() { + let account_proof: Vec = Vec::new(); + + let mpt = Trie::from_rlp(account_proof).unwrap(); + assert_eq!(mpt.hash_slow(), EMPTY_ROOT_HASH); + } + + #[cfg(feature = "serde")] + #[test] + fn parse_eth_get_proof_existing() { + // { "id": 1, "jsonrpc": "2.0", + // "method": "eth_getProof", + // "params": ["0x0000000000000000000000000000000000000004", [], "0x12962D1"] } + let value = serde_json::json!(["0xf90211a064fba17f021dbb0322d3e7d30aff9db628377c960f1ebed87701f08ce0b040eca09d91529d0a9cfb8e091b206bbcc359f7734dff6815c73e65ecbec4063508f9e9a0558f96de53974dabf223c2501c08c97dfc1c3d47a9b2c4ea0655df221c8154bea057fbe18660f4919b33d1dfbccd340a3d9ddae1a0e7d7df6f8df4b3cbe7c9d875a084f9dc2615d641d4136337942a7c76b94164b4ebd29422b860fa2251f82d2b73a05b6b9d6d421156c0282dfe73491c8754906849e989210d766fe4e4e266b32605a024eb3df5b1a9d8c6e40fb604542bc70e38e33f32c0f2feebdbd9b7e7e31ba7e4a015935675b64554bdc16b1c1cfc25c89f5d284ff11dcfbf7993aec54a92f0bba4a0099d5fc449ccc8c39482564ac0dd831f1e05387dae9dfd9dea51cb0d4e19aa8aa031d64c42ebfbcecb9f0220b752ab56f06b2682778d17119b7324c0ad96dfc149a0095fe3791c69f53ed524f8edbea71ac57efd64b9198810cc763efa0f4a5c2897a0515238447863a22615f154bc9c72e3aa4f69c4726ff41063a467ae84416731aaa022f3633a252b9b64f1bfcf48fc267570bd4203e4f36feb85dded2c1bd1cff3e0a042d3afbd98a8965f366b72e2213895467a77159c1c3d28c84a29013f8cb12873a07fd2b3663f9fc8d7836096d9369a233eb17532e85e69a953011f7d698b2cc00aa0726b3dbf33d6ad6a58d3c6007a706bbcba5442ad17eda4dd1934fe40279ad71080","0xf90211a0f5271d0b41d27321301a4a99ee222e2f6993733b8bb4297e5f4a193309cea441a0308c36c27bc35ce53fa864873bbdda24f8dda698a8829976c654d71aed28d65fa077780501dffccc355bf0353e3641740ba33c12291c4a8f2a240c7c5e8c0f0ddaa0d571e89fdf190b87f84db4bf25b50648758b6d86940e172622b93c6e818ca4dba076419b70cc41744498d74746de84ef31ae62a324a60cfcb9f38b438e31c9325ca0d41cdf14b7a848eb7b90d53aecee8b656bdc4f21f1285b40075fc2cfb09970dca0d268e7c26b2bf55597f7f2e6b1783ff59468590ef813e2d0c941c545ac947093a0aaaa06235ff457fc16692855eb8f6417895e85ee99030273e67ed434ec9edc78a093176f5004283b61b43c5bc0ac81a64d1f8fa13f03151040c25f0687fd661a20a01875442e8e36fb90a38cfe7fbc47071e6a8d73afe31310167facf68f13428509a0e964058555501ab03129574589ec66fbd34af7af4f9ede88dd20e6d509d3ed78a070094346b748e0d7fe73f52655307fed3967a9b8b5d12abcb86baa338bfce384a07b944e9ffc124854852a026deac8105902cb9bd96bd45ee6548566fcd9d0280ba0e9ec860d689a8471762d8bbacdc453de018634b0d80aed35eb0f72e5d0b4f841a0fca2e3962d69a6927845d1537f58b3871ea19e775ac88b973ffa7717c9f9b1dba0c3ef3351fb2e76a5130b103ee85d83a7fc658fc306d43d29a016f8e696b412db80","0xf90211a07a0bc6ce42efd1947af01f6573b633c431a5b0d14fd22e0704cb8556b7098fd3a0105680327ae064d660fe790b6bbb4f1453d6188eaafd707b2d0be6010275b3a6a0b41d2058d4c85808c887fd70c07ed2629c9f5f138d8dd512c060de9b07d2cd18a0639cbfec697a3c8461f4dd0f0eda8a58cb186db9dd6831d0d0a3ce9c6f99a7fea0a9f7964dec293389bc37f594cb03be415ec76208081bd61aa56bb83b8e749d1da0541091360f807b91263b692989e1a2df1aa894568661ea88359fd0ad76837131a02bea3a3d833e46c77b3214e753f59157d232d5f1c4d9f1b936e82f343ed62f48a07d6282c3a3353eaba6503faaa91c2efe85d5c72a80d209ad2c2648ea24449b7ba0c0576e98237780fea7677ef30e5e5b966f8874d2defa0f4e2bc95ac6e3180ffda0514d5942624e2309086cfde5c70cce77a13d3d8c7e04a299d9c98d4bd76fc80da0267e7598bb09c5965509b91763d4ddf7eb8baaa19276ef1630ff904ba22c5836a0a18ede2979de3c02978dbf1542c517a3eb42fe537fde09a3fb0ab46e139d0712a066b2679d23b911ea309551b9453115ae26d92fd05f8ac3f2e2e6e31cf6ac9dc7a0d541bc04f0feaa8bd4239d25cb04b1ddb5976d65a20bf91eae810f8a8fc33bdaa0d75b751ed0a1ef82c9cae91b2e97ea78c218d9a8923be953753962ef81b1aa9ba0cc3d9261f5ba93857bdac0d26a341984b45d3a6aa38811e4ced7a12594722a9380","0xf90211a028e51fc851a6315210c3e973bc0c37db45b9cbc38384235414e7f83bd06097f9a04fddedaebfa0a6e8f6bc1c035ff20c0365a92911e8a9b6f267621f729461fa68a0a2a92894c4f4f64cd8563f2c5945b3fc2857ec60bce8549d5871966510234310a00f8ec60cdce60eca2287e2c08b6a074cec1218b355e08504f8e0209095cf2caaa0a7f6689140ac7d1230f773d0e7f395c884d4c4be8e19c752650e94d59e63d492a061dfe74f4e14f2a06dfeb9f268acd81559c4ef17de098630432b2ff342441c36a00c450940c088ad97119ac6b9ecef673ccc41c4244bffac10744bdd2868811f15a09d405cc6b538d9a9033a96818608fb801d4f1a4319448e459a802aa966f0637ea0a604db8246efa1a62859269ff28709d950d74b55b4730002c5639e383a2867bca0f11de25a06de0a1162bffb3d400edf07a64659aa59c3554898700082866997fda02454c86901086b11a1b11a0f7ed300aeb451ca9d50509f7f8d1818cdaa42177ba00b06fa9d11f507136397be1aa73ec37c8bf8a7b10d1ccd07d0bcd9634ce469d0a001e5be1f99d16e7b0f12d73d01c7aa54e109186b18c7c1eb04a6784d619ca452a0676f68104f74c5f03d7b4638bbbb9a0ec647f34ae489de642eb9fa6c03388e20a048b07679ba600363323c2304443d2815ebe87e64bcee9f5ea31d97a0c4720c95a035d02f8a26fb9926254b3309ac3cacb94852a9a7469ab094e12b8df2d820dac680","0xf90211a0af5538b1a07c6b743b04ed3a04d1b20db89c831e2f2acfd54d92f710004c9495a0c3abf984d9d723ec0aaa2d2c6afb75b693ddbd5538a0709bbc17f8b17325b78fa051d2081439320734f889a4de79d72ec9d009bdd7b33c1d2970cb88c98d13d4c6a0b55856ee040bc79be42aa48f807651502a07ea73d46a63d2997a5ee1493b3372a0df7a231010a67f2286fbcc6e9347e0f0d7706bc23a95a08342d61c9f83eb93a8a07c0dd78b4a5a44ad1fb26fb11c710f8478f039b89df1f6c888d1b984bf51338ba075367e67992101f00c0d4ac7b90110c901ba1dc0bff377084ec3c933643019a3a0ecbb4272ff5b494973deb3ea559bd5296c0de719b6cbc71d4469dd1b0697aa87a0441af491d98cf36d5c259ad8a183348aba1f642a6c5d408c846d09f86b52882ea083641f2add799b0244eeab16095698b801dad630a60a769fb3f378346514f44ca05829539f9d1d5835d74a994278ff03ede1fde23997a2d4bacaf7a74c7081308da038b4f88c187767463cac005228d458a26554fa28174a82b2d6ea47a86991f026a08f9bf87dc8b2cfae0f21ee5e300b71310b5092268921ea387ed4d1d68ebb47c2a0b1a29582a77306d0a48675f4ee4603f01b3744993d1b057592c0b4af91ed5c73a065cc8133f26ec26a0053f978068a41075b138f7cd077452832f7fa1c79ec2250a08b3561166e1256df806bc6ba3c7f6312493b38133aa3524f51884b9115c7cc9780","0xf90211a0000aba28e5abf987658d245aabf1157eedee3f099ebf95affabe5a1e9b53b521a0c37da1b511fc6e5925e9b3e6870866da39bd3a880b45b3d830dacddd5ca5d1c1a054eb2e6cd765137c3ebde36d30decd3a9461e9245ac510405740a1307f700f8fa009b3401df44a885d6ab901ba7b789b290f7c3fbbdb2739ab487e33ff48bb6f13a0b124c2c227b22040df405b92eeeae70b28f9f59cb355220a0184c2d26aebf765a06f9217b6b53f3258db8c9b91cd05a07d2e90608c490e76dff6a847a7ead45859a00007fd17fdc3326a80364d9e820ca33580fa9e4641c3d9864a653c90515e3255a0110ad882ed1c35d31bca099524ca8da2aec305bdcc02cbee38f986f3bbfd6946a0d1a7c39603704486613fe6a83d8fff219e1b4d71cecd01b460703f4d0c4fee26a084a158747da12a4c0e1df62f95b206779f6da199a9832d22f79cc6017b14eeb5a070dbfff894a8269d76bb53722185466c879eb601fd9b6c413ef69f8b282959a8a0ac026666c540c02a02838e725f62a41356e6617ccf16a930db35ad4b253253c2a001c4c0478fdbaadc5ad58defe18fb9df49bdd12c9b8efbe7acfb11bd8deb52eaa0637e3ebb17f8bf5e8e9d2a987e00a10070cd8a8623d327fdd0fac545e453000ea0052155fc9e62a4a89b260a55f0d470b91b1e009dc8923292284f260a9ecf8785a04fb3083fdd53a3023fc4aec62f307f7e87cf0e4a771fba42cbe62a10ba98f40a80","0xf901b1a0d3c18050bdcb55f8e919d224eb69062be67a76708fb45d5f4263e2bf26473339a08c8749c75e158292e70ecf6defbd039c5b739733db1105430b065c3d40dbafbba02a33fef2d0dc98049fb6e1ab176ceef4cf4a376a62d9076e4557618200b3c00ba0534f68319d219b17e6c94295329c3a85478d182591e721dcb82ade11212e8ab2a0a4b88347eb9d7466e1cd2b7025a14bc53b93e39532fb60e276f24ce64f43080ea0c3b3a7be7982dcbb5680948ec6e80103109bb7adb312d6f738870894c0cd7cef80a007e513f4512674cfc773e861e6a34250eae5508f5a24b1af07e62583eecfc675a0f501c13e330dd417836ad1a304a578575b4618880a479b542953248c4aaef3bca0a5d05faed975c0134a4333809d471a6afb7b19f7db1a2e928ec0e27b5e5cf651a0f90433f3b2fa2fae1df8ce6b1c2cd794fe4fd8f899767d9aa59607285efbe17f80a0f2d6a5c820099a8212b3af56264bda3b519af5379f1a09f2a6e6dc412a1e6561a0c82e9f321649f50e95a89db8fbdec55babc1a552850ee7fff4d22d9aecb5710f80a0c189cfed2417dd103f87679e16d3c8e178d61d5da36bbdb2aeb4aa170a1560ba80","0xf85180a0c3b71af926a3b464d43b79c4f3b91835b055202902801ec32940cd45d78c3ed3a06d11221db3e0db5015e8b8c4f2e738c733a0b212a1399021e7824e5f908ecf578080808080808080808080808080","0xf85180a0cce18d0d1d7b4befb137e8b893e0d62e61cc7e43474d885853697bc6729e6544808080808080a0b17e5bdc4a7d0f184dde26b3a718143439522942254dd9bd109255cc49d3b0ab8080808080808080","0xf86d9c3a393dbd067dc72abfa08d475ed6447fca96d92ec3f9e7eba503ca61b84ef84c80881a5fd46f92e55070a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"]); + let account_proof = serde_json::from_value::>(value).unwrap(); + + let mpt = Trie::from_rlp(account_proof).unwrap(); + + let address = alloy_primitives::address!("0x0000000000000000000000000000000000000004"); + let account = mpt.get(keccak256(address)).map(|rlp| alloy_rlp::decode_exact(rlp).unwrap()); + assert_eq!( + account, + Some(alloy_trie::TrieAccount { + balance: alloy_primitives::uint!(0x1a5fd46f92e55070_U256), + ..Default::default() + }) + ); + } + + #[cfg(feature = "serde")] + #[test] + fn parse_eth_get_proof_nonexisting() { + // { "id": 1, "jsonrpc": "2.0", + // "method": "eth_getProof", + // "params": ["0x0010000000000000000000000000000000000000", [], "0x12962D1"] } + let value = serde_json::json!(["0xf90211a064fba17f021dbb0322d3e7d30aff9db628377c960f1ebed87701f08ce0b040eca09d91529d0a9cfb8e091b206bbcc359f7734dff6815c73e65ecbec4063508f9e9a0558f96de53974dabf223c2501c08c97dfc1c3d47a9b2c4ea0655df221c8154bea057fbe18660f4919b33d1dfbccd340a3d9ddae1a0e7d7df6f8df4b3cbe7c9d875a084f9dc2615d641d4136337942a7c76b94164b4ebd29422b860fa2251f82d2b73a05b6b9d6d421156c0282dfe73491c8754906849e989210d766fe4e4e266b32605a024eb3df5b1a9d8c6e40fb604542bc70e38e33f32c0f2feebdbd9b7e7e31ba7e4a015935675b64554bdc16b1c1cfc25c89f5d284ff11dcfbf7993aec54a92f0bba4a0099d5fc449ccc8c39482564ac0dd831f1e05387dae9dfd9dea51cb0d4e19aa8aa031d64c42ebfbcecb9f0220b752ab56f06b2682778d17119b7324c0ad96dfc149a0095fe3791c69f53ed524f8edbea71ac57efd64b9198810cc763efa0f4a5c2897a0515238447863a22615f154bc9c72e3aa4f69c4726ff41063a467ae84416731aaa022f3633a252b9b64f1bfcf48fc267570bd4203e4f36feb85dded2c1bd1cff3e0a042d3afbd98a8965f366b72e2213895467a77159c1c3d28c84a29013f8cb12873a07fd2b3663f9fc8d7836096d9369a233eb17532e85e69a953011f7d698b2cc00aa0726b3dbf33d6ad6a58d3c6007a706bbcba5442ad17eda4dd1934fe40279ad71080","0xf90211a07c0b2ddf03d5254f0a71793b61268dcccbcba1bbf91f84e1f01dfc7e748a22b3a0d066cbf287fff296cdf1c8d672f64cd1c7b582237a0e56f5523cadc9fd02db99a0ef75b0e082af37853216e2f1e189e8ed4a8fd8597d4cafb6a009d593d149463ba06b26667374d83ee094dab3adf971df2e3ec3f8255ce8e34b73d89afad4e5cc2ba0cc64a85fd14a5b27ec9aecd563395ea1c34629c7dca53222e7d19d1213af4d6ca07e3b3fd39452db1f8da9316152693d688505eff731e8b9c0e4512ba4a7882bcaa03579517fa320d080a74555da75ea17e676486aca21cac482f5f90993acec7112a0d674f6623ec3aaccde4856376e52b439ef4ebec600903c1e554b908b97780f64a04ebcd669406c6e48c0d1eca18c6c4040c8af886eac25e7218ef48d4cac8acf2ca0a4136773e1fdbde71ac578c4f5d25ee1c23067c20b43ea539b264fd566920630a0e94cab7b03de5bc128c5a15f3058c3c5ebebdd6f16e17548c2669886c1221b7aa0b1716052ef9a44fee9c985c4a957961c6d389bf290c82338e82cde8de5b174d5a096a699c048dc1d20509738881a99f1a09bc83a9ec0730b763c81c7ffb300b744a0733197a2479190e993f9da804810515c0e07b5752a1d0e537856cca86212a0f0a0d89157cef32fad06a43dd3ee54094a9790632d68cf07468ded845212ad484338a06055bbdb3664389c6e66a1519e99e802cb450d78fef9b8f8397c400d915ffe4780","0xf90211a001caaac143549f9bf7006ea276d133e2c6830bdb68d16a294a8aa836eeff19a2a0a68cdb682f254b8d8dfaf054bd571f4a84848cd58182274278c6dc09269093aaa063b4ef8b826676cea42d875d5a917193003ca99863a28dd4e4532307964c7e86a084473c41238e3dab60c7074c0fe4d6b14fec21124bdb999ef9fe92675d9e47bea01093a60823539cb5598a972ae1626dc68e62b83306cd0a7d0ca35bb0ae095432a0ea97146fe913ab3ffa242230eb2b0034f883de2a946f615cf4b152d8e4f9c2f1a0600c57157158dda437fdb93a2902d5a6aaadd2c4c4ef781b0f3bb9db52589ff1a005cd22b0467087c13203f302bd75ee3a49a4f50f72f044c510dfb80c5fd73d08a01a610490b54f297f9ed7f86993b19a4f1fe481d0bae5f8cc36428b066a19f675a002591f7ca3e129662132c3c43abec639309c7276bcb93f056ae0e064b08e544ba04cff311608aad7f75fe8660422dea95de1299b59ea1a92bc090a88c0a85033a1a0a23bc8dbe3fa661f80a211fbe0d46b938d681ed5c218391d1fc078242c52e07aa05152a90bb3bf83bc20c79d41e21eba4128916e93e3b2aa44e91d66419fed631ca04b9939df74f3585051cc7a256316931e8bbdba7ffadd42fc6a526acea2a65d5da0ad62bdb2eafbf12a569ab87cb4afd20853fd749a14817501bc051e16ace4b31da032a623bd0d0e0866790c76333c7279c455723c6c878d0a45767f9dd590f6f82b80","0xf90211a070740fa5e0ff39e4ecaa3807014386782670ead0ca3930db6e201e0633883524a0fae359c9f3636a34addbd37e75822169ddb824795f59a422a663466feabde3fca0ae96a399523295fb7628aa987c92c324aa579f7a6a0ef3215924f5ab0e2c7785a08b997d1e84063ff2b49c6fc7dda806333bec8a5616475b35021dc7330a6e63f7a076379dbbac470d9f0f8752f363dbb2dff2a0c0e1028d0a5d9aacdf5b735e9d4fa0da495111c40fed1ec3f6ae187ef71a8536a4aa6c09e84720f3645f1967267474a081250ce896356ce835178ed98f1b848a5b4c832f32c113e235020910437adcdba03d5338d4eb62c4d1bad3120bfff7c202a54c365abf2324ad0d97a04077b76900a03fea24d162e9725dc5e204fa2b8db528d0fc21ac54aa9c94e15105053e7bfd24a0c0f751477ad4b3a51a0af066821259e1a6ad3ac4a515c761765dd69552d07abfa079824c28b22d7b33105663cb8520d62101e303e9a0fabe00dfe2598654559b02a04be5cf039ac5c1bb6f8a4bf5dc3ceaa55e8e3fa8a9f6ae1953f744ef8c4c312ca0924250c41261da55a231889b7e88ff81428734661112d4faaa2961623ddd607ba0952fe6d321af6a7c322180c15f6aa06a8e3a461068f0e8c08183fd7536f9fd08a00b47228191943da98cc93b06f2475cb33de07d12d1445be308ffd94d786df047a0befcc93acb07cdf6240f884f7b4064f238c55f997a517f4bfcf6b970ea100d4e80","0xf90211a035ab3b8892330b5d3a92328b1c739c931c357dd4a5dc03e97ce130eb4d6ecf88a02ac2040a3839d2addee3deea351e02549e1a6662a5ff0ba42b8da6fd8964d58da0d5bef1786b6c185e65857742a860b724cba879fa0e8ae58abfab97e9ba213cc3a0649a68c768383bd53cd4b633b5297046bd8bf5c8b5e3f7be38b9b7eb037028e2a0247f186e30f069ed59215d7ed366c311a5bc5bde30ba52a8decc1d9145dcf286a0a725755aa59c849d7896625bbe2f31e0df98158be40adc6a394ffb1b4c05edcaa028a1af512637cfa63d06d9a416c46c6c8823be383a1ba1c1f880ade56b857ae6a070c12456255d06936ff8279cd8ed2de4596ff666161b8519fe8467310e88318fa033dfb37033f44cdce425c078ebe9f8ceb083f2bdb2058409c51c724140c4e99da0e38a0e9efc642da0375974ce69672ade21c5d43703ea5a38ad69644ccd01dc05a0f0773bd08b76b985fb3add2bf308fd1c548e22acb7eae8375f01f4c163f76be8a0e9cde81fdd404fb391b8ba5b3446e8c2e013b7792bc80f77703f2753552f470ea00975fa52a240800fd50ef22dad2aba6d1b4aeb19ee580ca13640d8a6de0936b9a07dd0437de64d7b5053478116ba0447ee5181818aadfb4a00ea1df67cc5d0bd1ca005f923facc0fb4445a5a488f628b353f3eb89936fb9e6b29bb1882e5c0881907a01d4fb8aac5a8d71ac5c0ed0c4491aed1532551451f630d717809797b4c3423b480","0xf90211a011d5405f5bf3648db2730954e75f7562daf4f93ae301482f3de752db8c6f6633a0f5dfe0e59a8b701cc7255480aba875bf4b2f8ec2b2a75b40320989d061f239ffa0641476041254ae3679696731e89da6e8ac4edc30762b30f0237014db2097ca98a0bca8b92c364aa974060a83482b3fddcbda788ace19d249e24225eb91cea050e3a08d5bc061d99b9803d93ad307aefb95950a9e5092cd3de0d642ec6b78d47ff883a093950c5e2655b226fccfd19359302b4e5a19e0d4812dc4859b0eadb3ee984118a02cc1641e3a8f95cea933cd6486d30e8d78d4a33fc81660b580ab05a67d13e438a0e9a7107962410d730bbc8f2350edbb592e7fb741dddee692c4bd9d4b3f1c16b4a05de66765b606c7bce1889f7c55028d19187f9ab9096e4d1927de1376c5905f2da03ca1622b70663d6880d0470191a28805547888ee1ece7ac65fd5c77dc67f6489a0cb515b40517b5c150115cdc0de89ed0e2a962a1ccdea09863b84050406fbd3eda0ff490436b20b1c8113eb98909c82c268e5454c8092ba0bc48a2b2903a0b4d372a07798003e5b7c0a905f0920e98bb17cf0a7f66fde92b0dd356463976cdda8c2e4a00c0fa4a0181bb622b9a4073844d5a72853b21e78db968ad4cea027828d402169a06b227ce534153d2d3952d3085da4f747e1c647f43ee616774df6f50c3f5646e5a0964a937d6cb948eaee92f05bd1a9d03af8ea3ee4e68b02ca9644d96ad86a0cd880","0xf901118080a02c5fbef8c93de59996332553693c43cf28dda1d13ac91dbee861a720883c7301808080a00cafef025cf50981161339913df13fac3635bb4d4612a6779b992b0896c1779480a0ad841e530360d785f7f258c646148f4b51093724f3b769efbb621fee609d2c3aa00abab5bb26960da6d85071fb6f12cf052c70b61c53acd72558ee06048a532128a043ae5b927740bbb05d9e6643a5c2e8c473cef67e529d431faf6edc79d19ba78a80a04a8ad4e57fe8d09e8596dc5131380ea3068decd740c1641ff712b35e37d6586980a045bd253469234b741abe3c8b110c04b0b9f3f983f752c394ef77814351a8d1bda08f74385b8385a4ebff237231c67a623e7bbad8151654e9edf32fa8464802cc6a80"]); + let account_proof = serde_json::from_value::>(value).unwrap(); + + let mpt = Trie::from_rlp(account_proof).unwrap(); + + let address = alloy_primitives::address!("0x0010000000000000000000000000000000000000"); + let account = mpt.get(keccak256(address)); + assert_eq!(account, None); + } + + mod cached { + use super::*; + use crate::CachedTrie; + + #[test] + fn index_trie() { + let leaves: Vec<(Vec, Bytes)> = (0..N) + .map(|i| { + let rlp = alloy_rlp::encode(i); + (rlp.clone(), rlp.into()) + }) + .collect(); + + // insert + let mut trie = CachedTrie::default(); + for (i, (key, value)) in leaves.iter().enumerate() { + trie.insert(key, value.clone()); + assert_eq!(trie.hash(), trie_root(leaves.iter().take(i + 1))); + } + + assert_eq!(trie.hash(), CachedTrie::from_rlp(trie.inner.rlp_nodes()).unwrap().hash()); + + // delete + for (i, (key, _)) in leaves.iter().enumerate() { + assert!(trie.remove(key)); + assert_eq!(trie.hash(), trie_root(leaves.iter().rev().take(N - 1 - i))); + } + } + + #[test] + fn keccak_trie() { + let leaves: Vec<(B256, Bytes)> = + (0..N).map(|i| (keccak256(i.to_be_bytes()), alloy_rlp::encode(i).into())).collect(); + + // insert + let mut trie = CachedTrie::default(); + for (i, (key, value)) in leaves.iter().enumerate() { + trie.insert(key, value.clone()); + assert_eq!(trie.hash(), trie_root(leaves.iter().take(i + 1))); + } + + assert_eq!(trie.hash(), CachedTrie::from_rlp(trie.inner.rlp_nodes()).unwrap().hash()); + + // delete + for (i, (key, _)) in leaves.iter().enumerate() { + assert!(trie.remove(key)); + assert_eq!(trie.hash(), trie_root(leaves.iter().rev().take(N - 1 - i))); + } + } + } +} diff --git a/crates/zeth-mpt/src/mpt/nibbles.rs b/crates/zeth-mpt/src/mpt/nibbles.rs new file mode 100644 index 0000000..f632727 --- /dev/null +++ b/crates/zeth-mpt/src/mpt/nibbles.rs @@ -0,0 +1,111 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A zero-cost abstraction for handling nibbles (4-bit values) as byte slices. +//! +//! This module provides `NibbleSlice`, a wrapper around `&[u8]` that guarantees +//! each byte represents a valid nibble (0-15). It offers efficient operations +//! for working with nibble data without runtime overhead. + +use alloy_primitives::hex; +use alloy_trie::Nibbles; +use core::{fmt, ops::Deref}; + +/// A slice of bytes representing nibbles. +#[derive(Clone, Copy)] +pub(super) struct NibbleSlice<'a>(&'a [u8]); + +impl Deref for NibbleSlice<'_> { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl fmt::Debug for NibbleSlice<'_> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Nibbles(0x{})", hex::encode(self.as_slice())) + } +} + +impl<'a> From<&'a Nibbles> for NibbleSlice<'a> { + /// Creates a `NibbleSlice` from a `Nibbles` reference. + #[inline] + fn from(nibbles: &'a Nibbles) -> Self { + Self(nibbles.as_slice()) + } +} + +impl From> for Nibbles { + /// Converts a `NibbleSlice` back into a `Nibbles`. + #[inline] + fn from(slice: NibbleSlice<'_>) -> Self { + Nibbles::from_nibbles_unchecked(slice.0) + } +} + +#[allow(dead_code)] +impl<'a> NibbleSlice<'a> { + #[inline] + pub(super) const fn as_slice(&self) -> &'a [u8] { + self.0 + } + + #[inline] + pub(super) const fn len(&self) -> usize { + self.0.len() + } + + #[inline] + pub(super) const fn is_empty(&self) -> bool { + self.0.is_empty() + } + + #[inline] + pub(super) fn join(&self, other: impl Into) -> Nibbles { + let other = other.into(); + let mut nibbles = Nibbles::with_capacity(self.len() + other.len()); + nibbles.extend_from_slice_unchecked(self.as_slice()); + nibbles.extend_from_slice_unchecked(other.as_slice()); + nibbles + } + + #[inline] + pub(super) fn split_first(&self) -> Option<(u8, Self)> { + self.0.split_first().map(|(nib, tail)| (*nib, Self(tail))) + } + + #[inline] + pub(super) fn strip_prefix(&self, prefix: &[u8]) -> Option { + self.0.strip_prefix(prefix).map(Self) + } + + #[inline] + pub(super) fn strip_suffix(&self, suffix: &[u8]) -> Option { + self.0.strip_suffix(suffix).map(Self) + } + + /// Splits `self` and `other` at the first nibble that differs. + #[inline] + pub(super) fn split_common_prefix(&self, other: impl Into) -> (Self, Self, Self) { + let (a, b) = (self.0, other.into().0); + let mid = a.iter().zip(b).take_while(|&(x, y)| x == y).count(); + // SAFETY: mid is the length of the common prefix: mid <= a.len() ∧ mid <= b.len() + let (common, a_tail) = unsafe { a.split_at_unchecked(mid) }; + (Self(common), Self(a_tail), Self(unsafe { b.split_at_unchecked(mid).1 })) + } +} diff --git a/crates/zeth-mpt/src/mpt/node.rs b/crates/zeth-mpt/src/mpt/node.rs new file mode 100644 index 0000000..9ec513f --- /dev/null +++ b/crates/zeth-mpt/src/mpt/node.rs @@ -0,0 +1,285 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + children::{Children, Entry}, + memoize::Memoization, + nibbles::NibbleSlice, +}; +use alloc::boxed::Box; +use alloy_primitives::{Bytes, B256}; +use alloy_trie::Nibbles; +use core::mem; + +pub(super) type Child = Box>; + +#[derive(Debug, Clone, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound(serialize = "", deserialize = "M: Default")) +)] +#[cfg_attr( + feature = "rkyv", + derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize), + rkyv(bytecheck(bounds(__C: rkyv::validation::ArchiveContext))), + rkyv(serialize_bounds(__S: rkyv::ser::Writer + rkyv::ser::Allocator, __S::Error: rkyv::rancor::Source)), + rkyv(deserialize_bounds(__D::Error: rkyv::rancor::Source)) +)] +pub(super) enum Node { + #[default] + Null, + Leaf( + #[cfg_attr(feature = "rkyv", rkyv(with = super::rkyv::NibblesDef))] Nibbles, + #[cfg_attr(feature = "rkyv", rkyv(with = super::rkyv::BytesDef))] Bytes, + #[cfg_attr(feature = "serde", serde(skip))] + #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))] + M, + ), + Extension( + #[cfg_attr(feature = "rkyv", rkyv(with = super::rkyv::NibblesDef))] Nibbles, + #[cfg_attr(feature = "rkyv", rkyv(omit_bounds))] Child, + #[cfg_attr(feature = "serde", serde(skip))] + #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))] + M, + ), + Branch( + Children, + #[cfg_attr(feature = "serde", serde(skip))] + #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Skip))] + M, + ), + Digest(#[cfg_attr(feature = "rkyv", rkyv(with = super::rkyv::B256Def))] B256), +} + +impl PartialEq for Node { + /// Equality between nodes ignores the cache. + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Node::Null, Node::Null) => true, + (Node::Leaf(n1, b1, _), Node::Leaf(n2, b2, _)) => n1 == n2 && b1 == b2, + (Node::Extension(n1, c1, _), Node::Extension(n2, c2, _)) => n1 == n2 && c1 == c2, + (Node::Branch(c1, _), Node::Branch(c2, _)) => c1 == c2, + (Node::Digest(d1), Node::Digest(d2)) => d1 == d2, + _ => false, // different variants are not equal + } + } +} + +impl Eq for Node {} + +impl Node { + /// Retrieves the value associated with a given key. + pub(super) fn get(&self, key: NibbleSlice<'_>) -> Option<&Bytes> { + match self { + Node::Null => None, + Node::Leaf(prefix, value, _) if prefix == key.as_slice() => Some(value), + Node::Leaf(..) => None, + Node::Extension(prefix, child, _) => { + key.strip_prefix(prefix).and_then(|tail| child.get(tail)) + } + Node::Branch(children, _) => match key.split_first() { + Some((nib, tail)) => { + // SAFETY: `key` is a `NibbleSlice` and thus only contains values < 0xf + let child = unsafe { children.get_unchecked(nib) }; + child.and_then(|node| node.get(tail)) + } + None => None, // branch nodes don't have values in our MPT version + }, + Node::Digest(_) => panic!("MPT: Unresolved node access"), + } + } + + /// Inserts a key-value pair into the trie. + pub(super) fn insert(&mut self, key: NibbleSlice<'_>, value: Bytes) { + assert!(!value.is_empty()); + match self { + Node::Null => { + *self = Node::Leaf(key.into(), value, M::default()); + } + Node::Leaf(prefix, leaf_val, cache) => { + let (common, key_rem, prefix_rem) = key.split_common_prefix(&*prefix); + if common.len() == prefix.len() && common.len() == key.len() { + *leaf_val = value; + cache.clear(); + return; + } else if common.len() == prefix.len() || common.len() == key.len() { + panic!("MPT: Value in branch"); + } + + let mut children = Children::default(); + match prefix_rem.split_first() { + Some((nib, tail)) => { + children.insert( + nib, + Node::Leaf(tail.into(), mem::take(leaf_val), M::default()).into(), + ); + } + None => unreachable!(), // mid < prefix.len() + } + match key_rem.split_first() { + Some((nib, tail)) => { + children.insert(nib, Node::Leaf(tail.into(), value, M::default()).into()) + } + None => unreachable!(), // mid < key.len() + }; + let branch = Node::Branch(children, M::default()); + + *self = if common.is_empty() { + branch + } else { + Node::Extension(common.into(), branch.into(), M::default()) + }; + } + Node::Extension(prefix, child, cache) => { + let (common, key_rem, prefix_rem) = key.split_common_prefix(&*prefix); + if common.len() == prefix.len() { + child.insert(key_rem, value); + cache.clear(); + return; + } else if common.len() == key.len() { + panic!("MPT: Value in branch"); + } + + let mut children = Children::default(); + match prefix_rem.as_slice() { + [nib] => children.insert(*nib, mem::take(child)), + [nib, tail @ ..] => { + // SAFETY: `tail` is a slice of `prefix` and thus only contains nibbles + let prefix = Nibbles::from_nibbles_unchecked(tail); + children.insert( + *nib, + Node::Extension(prefix, mem::take(child), M::default()).into(), + ); + } + _ => unreachable!(), // mid < prefix.len() + } + match key_rem.split_first() { + Some((nib, tail)) => { + children.insert(nib, Node::Leaf(tail.into(), value, M::default()).into()) + } + None => unreachable!(), // mid < key.len() + }; + let branch = Node::Branch(children, M::default()); + + *self = if common.is_empty() { + branch + } else { + Node::Extension(common.into(), branch.into(), M::default()) + }; + } + Node::Branch(children, cache) => match key.split_first() { + Some((nib, tail)) => match children.entry(nib) { + Entry::Occupied(mut entry) => { + entry.get_mut().insert(tail, value); + cache.clear(); + } + Entry::Vacant(entry) => { + entry.insert(Node::Leaf(tail.into(), value, M::default()).into()); + cache.clear(); + } + }, + None => panic!("MPT: Value in branch"), + }, + Node::Digest(_) => panic!("MPT: Unresolved node access"), + } + } + + /// Removes a key-value pair from the trie. + pub(super) fn remove(&mut self, key: NibbleSlice<'_>) -> bool { + match self { + Node::Null => false, + Node::Leaf(prefix, ..) if prefix == key.as_slice() => { + *self = Node::Null; + true + } + Node::Leaf(..) => false, + Node::Extension(prefix, child, cache) => { + if !key.strip_prefix(&*prefix).is_some_and(|tail| child.remove(tail)) { + return false; + } + cache.clear(); + + // an extension always points to a branch, if this has changed because of the remove + match **child { + Node::Null => *self = Node::Null, + Node::Leaf(ref extension, ref mut value, _) => { + prefix.extend_from_slice(extension); + *self = Node::Leaf(mem::take(prefix), mem::take(value), M::default()) + } + Node::Extension(ref extension, ref mut child, _) => { + prefix.extend_from_slice(extension); + *self = Node::Extension(mem::take(prefix), mem::take(child), M::default()) + } + Node::Branch(..) => {} + Node::Digest(_) => unreachable!(), // child.remove() would have panicked + } + true + } + Node::Branch(children, cache) => { + match key.split_first() { + Some((nib, tail)) => match children.entry(nib) { + Entry::Occupied(mut entry) => { + if !entry.get_mut().remove(tail) { + return false; + } + } + Entry::Vacant(_) => return false, + }, + None => return false, // branch nodes don't have values in our MPT version + }; + cache.clear(); + + if let Some((nib, only_child)) = children.take_single_child() { + match *only_child { + // if the only child is a leaf, prepend the corresponding nib to it + Node::Leaf(mut extension, value, _) => { + // SAFETY: `take_single_child` always returns a nibble + extension.as_mut_vec_unchecked().insert(0, nib); + *self = Node::Leaf(extension, value, M::default()); + } + // if the only child is an extension, prepend the corresponding nib to it + Node::Extension(mut extension, child, ..) => { + // SAFETY: `take_single_child` always returns a nibble + extension.as_mut_vec_unchecked().insert(0, nib); + *self = Node::Extension(extension, child, M::default()); + } + // if the only child is a branch, convert to an extension + Node::Branch(..) => { + // SAFETY: `take_single_child` always returns a nibble + let prefix = Nibbles::from_nibbles_unchecked([nib]); + *self = Node::Extension(prefix, only_child, M::default()); + } + Node::Digest(_) => panic!("MPT: Unresolved node access"), + Node::Null => unreachable!(), // children does not contain any Node::Null + } + } + true + } + Node::Digest(_) => panic!("MPT: Unresolved node access"), + } + } + + /// Returns the number of full nodes in the trie. + pub(super) fn size(&self) -> usize { + match self { + Node::Null | Node::Digest(_) => 0, + Node::Leaf(..) => 1, + Node::Extension(_, child, ..) => 1 + child.size(), + Node::Branch(children, ..) => { + 1 + children.iter().filter_map(Option::as_deref).map(Node::size).sum::() + } + } + } +} diff --git a/crates/zeth-mpt/src/mpt/orphan.rs b/crates/zeth-mpt/src/mpt/orphan.rs new file mode 100644 index 0000000..23a0208 --- /dev/null +++ b/crates/zeth-mpt/src/mpt/orphan.rs @@ -0,0 +1,305 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Functionality to resolve "orphan" nodes occurring during removes. +//! +//! Calling `remove` in sparse Merkle Patricia tries are only safe, if it does not lead to a Branch +//! node with just a single unresolved Digest child. Even though such a sparse trie is perfectly +//! valid to proof inclusion if the trie is not modified. + +use crate::{ + mpt::{memoize::Memoization, nibbles::NibbleSlice, node::Node}, + CachedTrie, Trie, +}; +use alloy_primitives::{keccak256, map::B256Map}; +use alloy_trie::Nibbles; + +/// Error returned by the `resolve_orphan` method. +#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] +pub enum Error { + /// Indicates that the proof does not have a valid RLP encoding. + #[error("proof RLP encoding error")] + RlpError(#[from] alloy_rlp::Error), + + /// Indicates that the given proof is an invalid post-removal proof and does not prove the + /// non-inclusion of the key. + #[error("invalid proof")] + InvalidProof, + + /// Indicates that the orphan cannot be resolved using only the provided post-removal proof. + /// This typically occurs when the removal of a key transforms an `Extension` node into a + /// `Branch` node, and the proof does not contain sufficient information to reconstruct the + /// original `Extension` node. + /// It contains the key prefix that needs to be resolved, to make the removal valid. + #[error("key prefix `{0:?}` not resolved")] + Unresolvable(Nibbles), +} + +impl Trie { + /// Attempts to resolve orphaned branch children caused by the removal of a key-value pair. + /// + /// When a key-value pair is removed from the trie, it may leave behind "orphaned" nodes that + /// must be transformed into a different type of node for the trie to remain valid. + /// This method uses an [EIP-1186](https://eips.ethereum.org/EIPS/eip-1186) proof to resolve + /// these orphans. The proof should represent the state of the trie *after* the removal of the + /// key-value pair. + /// + /// # Errors + /// + /// Returns `Ok(())` if the orphan was successfully resolved. Returns `Error` if the proof is + /// invalid or the orphan cannot be resolved with the given proof. + /// + /// # Panics + /// + /// It panics if the key is not contained in the trie. + #[inline] + pub fn resolve_orphan(&mut self, key: K, proof: I) -> Result<(), Error> + where + K: AsRef<[u8]>, + I: IntoIterator, + T: AsRef<[u8]>, + { + self.0.resolve_orphan(NibbleSlice::from(&Nibbles::unpack(key)), proof) + } +} + +impl CachedTrie { + /// Attempts to resolve orphaned branch children caused by removing a key-value pair. + /// + /// See [`Trie::resolve_orphan`] for detailed documentation. + #[inline] + pub fn resolve_orphan(&mut self, key: K, proof: I) -> Result<(), Error> + where + K: AsRef<[u8]>, + I: IntoIterator, + T: AsRef<[u8]>, + { + self.inner.resolve_orphan(NibbleSlice::from(&Nibbles::unpack(key)), proof) + } +} + +impl Node { + /// Attempts to resolve orphaned branch children caused by removing a key-value pair. + pub(super) fn resolve_orphan>( + &mut self, + key: NibbleSlice<'_>, + proof: impl IntoIterator, + ) -> Result<(), Error> { + assert!(self.get(key).is_some(), "key not contained"); + let other = Node::from_rlp(proof)?; + let Some((diverging, unmatched)) = other.diverging(key) else { + return Ok(()); + }; + let matched = key.strip_suffix(&unmatched).unwrap(); + + match diverging { + Node::Null => { + // the entire tree has been removed so trivially there can be no orphans + } + Node::Leaf(prefix, value, _) => { + // get the unmatched part of the Leaf-prefix + let (common, unmatched, _) = + NibbleSlice::from(prefix).split_common_prefix(unmatched); + // split the first nibble which used to belong to the Branch + let (idx, suffix) = unmatched.split_first().expect("empty unmatched key"); + // this can only be an orphan, if it is currently a Digest child of a Branch + if !self.is_branch_with_digest(&matched.join(common), idx) { + return Ok(()); + } + + // any orphan must be a Leaf with the suffix as a prefix + let sibling = Node::Leaf(suffix.into(), value.clone(), M::default()); + let rlp = sibling.rlp_encoded(); + self.resolve_digests(&B256Map::from_iter([(keccak256(&rlp), rlp)])).unwrap(); + } + Node::Extension(prefix, child, _) => { + // get the unmatched part of the Extension-prefix + let (common, unmatched, _) = + NibbleSlice::from(prefix).split_common_prefix(unmatched); + // split the first nibble which used to belong to the Branch + let (idx, suffix) = unmatched.split_first().expect("empty unmatched key"); + // this can only be an orphan, if it is currently a Digest child of a Branch + if !self.is_branch_with_digest(&matched.join(common), idx) { + return Ok(()); + } + + // Extensions cannot have an empty prefix. This means that if the suffix is empty, + // the orphan is a Branch, and because of the removal, its parent Branch has been + // converted to an Extension. So to resolve this orphan, we need to know the + // original Branch. + if suffix.is_empty() { + // if we are lucky, the post-removal proof does not stop at the Extension and + // the child still corresponds to the node we are looking for. + if !matches!(**child, Node::Digest(_)) { + let rlp = child.rlp_encoded(); + self.resolve_digests(&B256Map::from_iter([(keccak256(&rlp), rlp)])) + .unwrap(); + } + // the path to the orphan corresponds exactly to the path of the Extension-child + let orphan_prefix = matched.join(prefix); + // maybe the trie already contains a node with this prefix + if self.contains_prefix(&orphan_prefix) { + // in this case, the removal will not fail and nothing needs to be resolved + return Ok(()); + } + // otherwise return error that the given prefix needs to be resolved externally + return Err(Error::Unresolvable(orphan_prefix)); + } + + // any potential orphan must be an Extension with the (non-empty) suffix as a prefix + let sibling = Node::Extension(suffix.into(), (*child).clone(), M::default()); + let rlp = sibling.rlp_encoded(); + self.resolve_digests(&B256Map::from_iter([(keccak256(&rlp), rlp)])).unwrap(); + } + Node::Digest(_) => { + // the proof is invalid, as it does not proof the non-inclusion of `key` + return Err(Error::InvalidProof); + } + Node::Branch(..) => unreachable!("Branch node with value"), + } + + Ok(()) + } + + /// Returns the diverging trie node for a key. + /// + /// If the key is present in the trie, this method returns `None`. Otherwise, it returns the + /// node where the search for the key would fail, along with the unmatched portion of the key. + fn diverging<'a>(&'a self, key: NibbleSlice<'a>) -> Option<(&'a Node, NibbleSlice<'a>)> { + match self { + Node::Null => Some((&Node::Null, key)), + + Node::Leaf(prefix, ..) if prefix == key.as_slice() => None, + Node::Leaf(..) => Some((self, key)), + + Node::Extension(prefix, child, _) => { + key.strip_prefix(prefix).map_or(Some((self, key)), |tail| child.diverging(tail)) + } + + Node::Branch(children, _) => match key.split_first() { + Some((idx, tail)) => { + let child = children.get(idx); + child.map_or(Some((&Node::Null, tail)), |node| node.diverging(tail)) + } + None => Some((self, key)), // branch nodes don't have values + }, + + Node::Digest(_) => Some((self, key)), + } + } + + fn contains_prefix<'a>(&'a self, key: impl Into>) -> bool { + match self.diverging(key.into()) { + None => true, // contains the prefix as a key + Some((Node::Digest(_), _)) => false, // prefix not resolved + Some((_, unmatched)) => unmatched.is_empty(), // prefix contained or not + } + } + + /// Returns whether the node at key is a Branch which has a Digest child at idx. + fn is_branch_with_digest<'a>(&'a self, key: impl Into>, idx: u8) -> bool { + match self.diverging(key.into()) { + // match only if, the node found is a `Node::Branch` and the *entire* key was consumed + Some((Node::Branch(children, ..), unmatched)) if unmatched.is_empty() => { + // if all the above conditions are met, check the specific child + matches!(children.get(idx), Some(Node::Digest(_))) + } + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Trie; + use alloc::{vec, vec::Vec}; + use alloy_primitives::{Bytes, B256}; + use alloy_trie::{proof::ProofRetainer, HashBuilder, Nibbles}; + use core::borrow::Borrow; + use std::panic; + + fn create_eip1186_proof( + key: K, + trie: impl IntoIterator>, + ) -> Vec + where + K: AsRef<[u8]>, + V: AsRef<[u8]>, + { + let hb = HashBuilder::default(); + let mut hb = + hb.with_proof_retainer(ProofRetainer::new(vec![Nibbles::unpack(key.as_ref())])); + + let mut sorted_data: Vec<_> = trie.into_iter().collect(); + sorted_data.sort_by(|a, b| a.borrow().0.as_ref().cmp(b.borrow().0.as_ref())); + for (key, val) in sorted_data.iter().map(Borrow::borrow) { + hb.add_leaf(Nibbles::unpack(key), val.as_ref()); + } + let _ = hb.root(); + + hb.take_proof_nodes().into_nodes_sorted().into_iter().map(|(_, rlp)| rlp).collect() + } + + #[test] + fn leaf_orphan() { + let keys = [vec![0x00], vec![0x11]]; + let key = &keys[0]; + let leaves = keys.iter().map(|k| (k, Bytes::from(B256::ZERO))).collect::>(); + + let proof = create_eip1186_proof(key, &leaves); + let post_proof = create_eip1186_proof(key, &leaves[1..]); + + let mut trie = Trie::from_rlp(proof).unwrap(); + assert!(trie.get(key).is_some()); + assert!(panic::catch_unwind(|| trie.clone().remove(key)).is_err(), "Removal should panic"); + + trie.resolve_orphan(key, post_proof).unwrap(); + trie.remove(key); + } + + #[test] + fn extension_orphan() { + let keys = [vec![0x00], vec![0x10, 0x00], vec![0x10, 0x01]]; + let key = &keys[0]; + let leaves = keys.iter().map(|k| (k, Bytes::from(B256::ZERO))).collect::>(); + + let proof = create_eip1186_proof(key, &leaves); + let post_proof = create_eip1186_proof(key, &leaves[1..]); + + let mut trie = Trie::from_rlp(proof).unwrap(); + assert!(trie.get(key).is_some()); + assert!(panic::catch_unwind(|| trie.clone().remove(key)).is_err(), "Removal should panic"); + + trie.resolve_orphan(key, post_proof).unwrap(); + trie.remove(key); + } + + #[test] + fn unresolvable_orphan() { + let keys = [vec![0x00], vec![0x10], vec![0x11]]; + let key = &keys[0]; + let leaves = keys.iter().map(|k| (k, Bytes::from(B256::ZERO))).collect::>(); + + let proof = create_eip1186_proof(key, &leaves); + let post_proof = create_eip1186_proof(key, &leaves[1..]); + + let mut trie = Trie::from_rlp(proof).unwrap(); + assert!(trie.get(key).is_some()); + assert!(panic::catch_unwind(|| trie.clone().remove(key)).is_err(), "Removal should panic"); + + let err = trie.resolve_orphan(key, post_proof).unwrap_err(); + assert!(matches!(err, Error::Unresolvable(_))); + } +} diff --git a/crates/zeth-mpt/src/mpt/rkyv.rs b/crates/zeth-mpt/src/mpt/rkyv.rs new file mode 100644 index 0000000..c723d7f --- /dev/null +++ b/crates/zeth-mpt/src/mpt/rkyv.rs @@ -0,0 +1,181 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + memoize::{Cache, Memoization}, + node::Node, +}; +use alloc::vec::Vec; +use alloy_primitives::{Bytes, B256}; +use alloy_trie::nybbles::Nibbles; +use rkyv::{ + rancor::{Fallible, Source}, + ser::{Allocator, Writer}, + vec::{ArchivedVec, VecResolver}, + with::{ArchiveWith, DeserializeWith, SerializeWith}, + Archive, Archived, Deserialize, Place, Serialize, +}; + +/// Wrapper to encode a [B256] as an `[u8; 32]`. +#[derive(Archive, Serialize, Deserialize)] +#[rkyv(remote = B256)] +pub(super) struct B256Def([u8; B256::len_bytes()]); + +impl From for B256 { + #[inline] + fn from(B256Def(arr): B256Def) -> Self { + Self(arr) + } +} + +/// Wrapper to encode [Bytes] as an [`ArchivedVec`]. +pub(super) struct BytesDef; + +impl ArchiveWith for BytesDef { + type Archived = ArchivedVec; + type Resolver = VecResolver; + + fn resolve_with(bytes: &Bytes, resolver: Self::Resolver, out: Place) { + ArchivedVec::resolve_from_slice(bytes, resolver, out); + } +} + +impl SerializeWith for BytesDef { + fn serialize_with(bytes: &Bytes, serializer: &mut S) -> Result { + ArchivedVec::serialize_from_slice(bytes, serializer) + } +} + +impl DeserializeWith>, Bytes, D> for BytesDef +where + D: Fallible + ?Sized, + ::Error: Source, +{ + fn deserialize_with(field: &ArchivedVec, deserializer: &mut D) -> Result { + let vec = as Deserialize, D>>::deserialize(field, deserializer)?; + Ok(Bytes::from(vec)) + } +} + +/// Wrapper to encode [Nibbles] as an [`ArchivedVec`]. +pub(super) struct NibblesDef; + +impl ArchiveWith for NibblesDef { + type Archived = ArchivedVec; + type Resolver = VecResolver; + + fn resolve_with(nibbles: &Nibbles, resolver: Self::Resolver, out: Place) { + ArchivedVec::resolve_from_slice(nibbles, resolver, out); + } +} + +impl SerializeWith for NibblesDef { + fn serialize_with(nibbles: &Nibbles, serializer: &mut S) -> Result { + ArchivedVec::serialize_from_slice(nibbles, serializer) + } +} + +impl DeserializeWith>, Nibbles, D> for NibblesDef +where + D: Fallible + ?Sized, + ::Error: Source, +{ + fn deserialize_with(f: &ArchivedVec, deserializer: &mut D) -> Result { + let vec = as Deserialize, D>>::deserialize(f, deserializer)?; + Ok(Nibbles::from_vec_unchecked(vec)) + } +} + +/// RLP-encodes a cached trie during serialization. +/// +/// This has several advantages: +/// - The serialized bytes are fully verified at deserialization. +/// - The trie nodes already have an RLP-encoding when the hash is computed. +#[derive(Archive, Serialize, Deserialize)] +#[rkyv(remote = Node)] +pub(super) struct RlpNodes(#[rkyv(getter = rlp_nodes)] Vec>); + +fn rlp_nodes(node: &Node) -> Vec> { + let mut unique = Vec::new(); + for node in node.rlp_nodes() { + if !unique.contains(&node) { + unique.push(node); + } + } + + unique.into_iter().map(Vec::from).collect() +} + +impl From for Node { + #[inline] + fn from(RlpNodes(nodes): RlpNodes) -> Self { + Node::from_rlp(nodes).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mpt::{ArchivedTrie, Trie}; + use alloy_primitives::keccak256; + use rkyv::rancor::Error; + + const N: usize = 512; + + #[test] + fn round_trip() { + let trie: Trie = (0..N) + .map(|i| (keccak256(i.to_be_bytes()), Bytes::from(alloy_rlp::encode(i)))) + .collect(); + + let bytes = rkyv::to_bytes::(&trie).unwrap(); + let archived = rkyv::access::(&bytes).unwrap(); + let other = rkyv::deserialize::(archived).unwrap(); + + assert_eq!(trie, other); + } + + mod cached { + use super::*; + use crate::mpt::{ArchivedCachedTrie, CachedTrie}; + + #[test] + fn round_trip() { + let mut trie: CachedTrie = (0..N) + .map(|i| (keccak256(i.to_be_bytes()), Bytes::from(alloy_rlp::encode(i)))) + .collect(); + trie.hash(); + assert!(trie.hash.is_some()); + + let bytes = rkyv::to_bytes::(&trie).unwrap(); + let archived = rkyv::access::(&bytes).unwrap(); + let other = rkyv::deserialize::(archived).unwrap(); + assert!(other.hash.is_none()); + + assert_eq!(trie, other); + } + + #[test] + fn round_trip_dup() { + let trie: CachedTrie = + (0..255).map(|i| (B256::with_last_byte(i), Bytes::from(B256::ZERO))).collect(); + + let bytes = rkyv::to_bytes::(&trie).unwrap(); + let archived = rkyv::access::(&bytes).unwrap(); + let other = rkyv::deserialize::(archived).unwrap(); + + assert_eq!(trie, other); + } + } +} diff --git a/crates/zeth-mpt/src/mpt/rlp.rs b/crates/zeth-mpt/src/mpt/rlp.rs new file mode 100644 index 0000000..d3af2a0 --- /dev/null +++ b/crates/zeth-mpt/src/mpt/rlp.rs @@ -0,0 +1,445 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + children::{Children, Entry}, + memoize::Memoization, + node::Node, +}; +use alloc::{vec, vec::Vec}; +use alloy_primitives::{ + hex, keccak256, + map::{B256HashMap, B256Map}, + Bytes, B256, +}; +use alloy_rlp::{BufMut, Decodable, Encodable, Header, PayloadView, EMPTY_STRING_CODE}; +use alloy_trie::{nodes::encode_path_leaf, Nibbles, EMPTY_ROOT_HASH}; +use arrayvec::ArrayVec; +use core::fmt; + +/// The length in bytes of an RLP-encoded digest, i.e. hash length + 1 byte for the RLP header. +const DIGEST_RLP_LENGTH: usize = 1 + B256::len_bytes(); + +impl Node { + /// Returns the hash of the node. + #[inline] + pub(super) fn hash(&self) -> B256 { + NodeRef::from_node(self).hash() + } + + /// Returns the RLP encoding of the node. + pub(super) fn rlp_encoded(&self) -> Vec { + match self { + Node::Null => vec![EMPTY_STRING_CODE], + Node::Leaf(prefix, value, _) => { + let path = encode_path_leaf(prefix, true); + let mut out = encode_list_header(path.length() + value.length()); + path.encode(&mut out); + value.encode(&mut out); + + out + } + Node::Extension(prefix, child, _) => { + let path = encode_path_leaf(prefix, false); + let node_ref = NodeRef::from_node(child); + let mut out = encode_list_header(path.length() + node_ref.length()); + path.encode(&mut out); + node_ref.encode(&mut out); + + out + } + Node::Branch(children, _) => { + let mut child_refs: [NodeRef<'_>; 16] = Default::default(); + let mut payload_length = 1; // start with 1 for the EMPTY_STRING_CODE at the end + + for (i, child) in children.iter().enumerate() { + match child { + Some(node) => { + let node_ref = NodeRef::from_node(node); + payload_length += node_ref.length(); + child_refs[i] = node_ref; + } + None => payload_length += 1, + } + } + + let mut out = encode_list_header(payload_length); + child_refs.iter().for_each(|child| child.encode(&mut out)); + // add an EMPTY_STRING_CODE for the missing value + out.push(EMPTY_STRING_CODE); + + out + } + Node::Digest(digest) => alloy_rlp::encode(digest), + } + } + + /// Memoize the hash of every sub-trie. + pub(super) fn memoize(&mut self) { + // early termination for already memoized nodes or Null/Digest + match self { + Node::Leaf(.., cache) | Node::Extension(.., cache) | Node::Branch(.., cache) + if cache.get().is_some() => + { + return; + } + Node::Null | Node::Digest(_) => return, + _ => {} // proceed to memoization for other variants + } + match self { + Node::Extension(_, child, _) => child.memoize(), + Node::Branch(children, _) => children.memoize(), + _ => {} // no children to memoize for Leaf, Null, or Digest + } + let rlp = self.rlp_encoded(); + match self { + Node::Leaf(.., cache) | Node::Extension(.., cache) | Node::Branch(.., cache) => { + cache.set(RlpNode::from_rlp(rlp)); + } + _ => unreachable!(), + } + } + + /// Returns the RLP-encoded nodes of the trie in preorder. + pub(super) fn rlp_nodes(&self) -> Vec { + fn rec<'a, M: Memoization>(node: &'a Node, nodes: &mut Vec) -> NodeRef<'a> { + let node_ref = match node { + Node::Extension(prefix, child, _) => { + let (path, child) = (encode_path_leaf(prefix, false), rec(child, nodes)); + let mut out = encode_list_header(path.length() + child.length()); + path.encode(&mut out); + child.encode(&mut out); + NodeRef::Rlp(out) + } + Node::Branch(children, _) => { + let mut list = Vec::with_capacity(17); + for child in children.iter() { + let node_ref = child.as_ref().map_or(NodeRef::Empty, |c| rec(c, nodes)); + list.push(node_ref); + } + list.push(NodeRef::Empty); + NodeRef::Rlp(encode_list(&list)) + } + Node::Leaf(..) => NodeRef::Rlp(node.rlp_encoded()), // do not use the cached value + Node::Digest(digest) => NodeRef::Digest(digest), + Node::Null => NodeRef::Empty, + }; + match &node_ref { + NodeRef::Rlp(rlp) if rlp.len() >= 32 => nodes.push(rlp.clone().into()), + NodeRef::Cached(..) => unreachable!(), + _ => {} + } + node_ref + } + + if matches!(self, Node::Null) { + return vec![]; + } + + let mut vec = Vec::new(); + match rec(self, &mut vec) { + NodeRef::Rlp(rlp) if rlp.len() >= 32 => {} + NodeRef::Cached(..) => unreachable!(), + node_ref => vec.push(alloy_rlp::encode(node_ref).into()), + } + vec.reverse(); + + vec + } + + /// Creates a new trie from the given RLP encoded nodes. + pub(super) fn from_rlp>( + nodes: impl IntoIterator, + ) -> alloy_rlp::Result { + let mut iterator = nodes.into_iter(); + + // the first node must be the root + let mut root = match iterator.next() { + None => return Ok(Self::default()), + Some(rlp) => { + let mut node: Node = alloy_rlp::decode_exact(rlp.as_ref())?; + node.cache_set(RlpNode::from_rlp(rlp)); + node + } + }; + + // compute the references of all the remaining nodes + let (lower, _) = iterator.size_hint(); + let mut rlp_by_digest = B256HashMap::with_capacity_and_hasher(lower, Default::default()); + for rlp in iterator { + rlp_by_digest.insert(keccak256(&rlp), rlp); + } + + // return the resolved trie + root.resolve_digests(&rlp_by_digest)?; + Ok(root) + } + + /// Resolves all applicable digest nodes with the node corresponding to the RLP encoding. + pub(super) fn resolve_digests( + &mut self, + rlp_by_digest: &B256Map>, + ) -> alloy_rlp::Result<()> { + match self { + Node::Null | Node::Leaf(..) => {} + Node::Extension(_, child, _) => { + child.resolve_digests(rlp_by_digest)?; + if !matches!(**child, Node::Branch(..) | Node::Digest(..)) { + return Err(alloy_rlp::Error::Custom("extension node with invalid child")); + } + } + Node::Branch(children, _) => { + for entry in children.entries() { + if let Entry::Occupied(mut entry) = entry { + entry.get_mut().resolve_digests(rlp_by_digest)?; + } + } + } + Node::Digest(digest) => { + if let Some(bytes) = rlp_by_digest.get(digest) { + let mut node: Node = alloy_rlp::decode_exact(bytes.as_ref())?; + // do not try to replace a node by a digest + if !matches!(node, Node::Digest(_)) { + node.cache_set(RlpNode::from_digest(digest)); + *self = node; + self.resolve_digests(rlp_by_digest)?; + } + } + } + } + + Ok(()) + } + + #[inline] + fn cache_set(&mut self, rlp_node: RlpNode) { + match self { + Node::Leaf(.., cache) | Node::Extension(.., cache) | Node::Branch(.., cache) => { + cache.set(rlp_node) + } + _ => {} + } + } +} + +impl Decodable for Node { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + match Header::decode_raw(buf)? { + // if the node is not a list, it must be empty or a digest + PayloadView::String(payload) => match payload.len() { + 0 => Ok(Node::Null), + 32 => Ok(Node::Digest(B256::from_slice(payload))), + _ => Err(alloy_rlp::Error::UnexpectedLength), + }, + PayloadView::List(items) => match items.len() { + // branch node: 17-item node [ v0 ... v15, value ] + 17 => { + let mut children = Children::default(); + for (i, child_rlp) in items.iter().enumerate() { + if child_rlp != &[EMPTY_STRING_CODE] { + if i == 16 { + return Err(alloy_rlp::Error::Custom("branch node with value")); + } else { + children.insert(i as u8, Node::decode(&mut &child_rlp[..])?.into()); + } + } + } + if children.len() < 2 { + return Err(alloy_rlp::Error::Custom("branch node without two children")); + } + + Ok(Node::Branch(children, M::default())) + } + // leaf or extension node: 2-item node [ encodedPath, v ] + // they are distinguished by a flag in the first nibble of the encodedPath + 2 => { + let [mut encode_path, mut v] = items.as_slice() else { unreachable!() }; + let (path, is_leaf) = decode_path(&mut encode_path)?; + if is_leaf { + Ok(Node::Leaf(path, Bytes::decode(&mut v)?, M::default())) + } else { + let node = Node::decode(&mut v)?; + if !matches!(node, Node::Branch(..) | Node::Digest(..)) { + return Err(alloy_rlp::Error::Custom( + "extension node with invalid child", + )); + } + Ok(Node::Extension(path, node.into(), M::default())) + } + } + _ => Err(alloy_rlp::Error::Custom("unexpected list length")), + }, + } + } +} + +/// An RLP-encoded node. +#[derive(Clone)] +pub(super) struct RlpNode(ArrayVec); + +impl RlpNode { + #[inline] + fn from_rlp(rlp: impl AsRef<[u8]>) -> Self { + let rlp = rlp.as_ref(); + if rlp.len() >= B256::len_bytes() { + Self(alloy_rlp::encode_fixed_size(&keccak256(rlp))) + } else { + let mut arr = ArrayVec::new(); + // SAFETY: rlp.len() < 32 < DIGEST_RLP_LENGTH + unsafe { arr.try_extend_from_slice(rlp).unwrap_unchecked() }; + Self(arr) + } + } + + #[inline] + fn from_digest(digest: &B256) -> Self { + Self(alloy_rlp::encode_fixed_size(digest)) + } + + #[inline] + fn hash(&self) -> B256 { + if self.0.len() == DIGEST_RLP_LENGTH { + B256::from_slice(&self.0[1..]) + } else { + keccak256(&self.0) + } + } +} + +impl fmt::Debug for RlpNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{}", hex::encode(&self.0)) + } +} + +impl Encodable for RlpNode { + #[inline] + fn encode(&self, out: &mut dyn BufMut) { + out.put_slice(&self.0) + } + + #[inline] + fn length(&self) -> usize { + self.0.len() + } +} + +/// Represents the way in which a node is referenced from within another node. +#[derive(Default)] +enum NodeRef<'a> { + #[default] + Empty, + Digest(&'a B256), + Cached(&'a RlpNode), + Rlp(Vec), +} + +impl NodeRef<'_> { + #[inline] + fn from_node(node: &Node) -> NodeRef<'_> { + match node { + Node::Null => NodeRef::Empty, + Node::Digest(digest) => NodeRef::Digest(digest), + Node::Leaf(.., cache) | Node::Extension(.., cache) | Node::Branch(.., cache) => { + cache.get().map_or_else(|| NodeRef::Rlp(node.rlp_encoded()), NodeRef::Cached) + } + } + } + + #[inline] + fn hash(&self) -> B256 { + match self { + NodeRef::Empty => EMPTY_ROOT_HASH, + NodeRef::Digest(&digest) => digest, + NodeRef::Cached(rlp_node) => rlp_node.hash(), + NodeRef::Rlp(rlp) => keccak256(rlp), + } + } +} + +impl Encodable for NodeRef<'_> { + #[inline] + fn encode(&self, out: &mut dyn BufMut) { + match self { + NodeRef::Empty => out.put_u8(EMPTY_STRING_CODE), + NodeRef::Digest(digest) => digest.encode(out), + NodeRef::Cached(rlp_node) => rlp_node.encode(out), + NodeRef::Rlp(rlp) => { + if rlp.len() >= B256::len_bytes() { + keccak256(rlp).encode(out); + } else { + out.put_slice(rlp); + } + } + } + } + + #[inline] + fn length(&self) -> usize { + match self { + NodeRef::Empty => 1, + NodeRef::Digest(_) => DIGEST_RLP_LENGTH, + NodeRef::Cached(rlp_node) => rlp_node.length(), + NodeRef::Rlp(rlp) => { + if rlp.len() >= B256::len_bytes() { + DIGEST_RLP_LENGTH + } else { + rlp.len() + } + } + } + } +} + +#[inline] +fn encode_list_header(payload_length: usize) -> Vec { + debug_assert!(payload_length > 1); + let header = Header { list: true, payload_length }; + let mut out = Vec::with_capacity(header.length() + payload_length); + header.encode(&mut out); + out +} + +#[inline] +fn decode_path(buf: &mut &[u8]) -> alloy_rlp::Result<(Nibbles, bool)> { + let path = Nibbles::unpack(Header::decode_bytes(buf, false)?); + if path.len() < 2 { + return Err(alloy_rlp::Error::InputTooShort); + } + let (is_leaf, odd_nibbles) = match path[0] { + 0b0000 => (false, false), + 0b0001 => (false, true), + 0b0010 => (true, false), + 0b0011 => (true, true), + _ => return Err(alloy_rlp::Error::Custom("node is not an extension or leaf")), + }; + let prefix = if odd_nibbles { &path[1..] } else { &path[2..] }; + Ok((Nibbles::from_nibbles_unchecked(prefix), is_leaf)) +} + +fn encode_list(values: &[B]) -> Vec +where + B: core::borrow::Borrow, + T: ?Sized + Encodable, +{ + let mut payload_length = 0; + for value in values { + payload_length += value.borrow().length(); + } + let mut out = encode_list_header(payload_length); + for value in values { + value.borrow().encode(&mut out); + } + out +} diff --git a/crates/zeth-mpt/src/mpt/serde.rs b/crates/zeth-mpt/src/mpt/serde.rs new file mode 100644 index 0000000..268a3c7 --- /dev/null +++ b/crates/zeth-mpt/src/mpt/serde.rs @@ -0,0 +1,109 @@ +// Copyright 2025 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// RLP-encodes a cached trie during serialization. +/// +/// This has several advantages: +/// - The serialized bytes are fully verified at deserialization. +/// - The trie nodes already have an RLP-encoding when the hash is computed. +#[cfg(feature = "rlp_serialize")] +pub(crate) mod rlp_nodes { + use crate::mpt::{memoize::Memoization, node::Node}; + use alloc::vec::Vec; + use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serializer}; + + #[inline] + pub(crate) fn serialize(trie: &Node, serializer: S) -> Result + where + S: Serializer, + M: Memoization, + { + // deduplicate the RLP nodes + let mut nodes = Vec::new(); + for node in trie.rlp_nodes() { + if !nodes.contains(&node) { + nodes.push(node); + } + } + + let mut seq = serializer.serialize_seq(Some(nodes.len()))?; + for node in &nodes { + seq.serialize_element(&node[..])?; + } + seq.end() + } + + #[inline] + pub(crate) fn deserialize<'de, D, M>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + M: Memoization, + { + let nodes: Vec<&[u8]> = Vec::deserialize(deserializer)?; + + Node::from_rlp(nodes).map_err(de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use crate::Trie; + use alloy_primitives::{keccak256, Bytes}; + + const N: usize = 512; + + #[test] + fn round_trip() { + let trie: Trie = (0..N) + .map(|i| (keccak256(i.to_be_bytes()), Bytes::from(alloy_rlp::encode(i)))) + .collect(); + + let bytes = bincode::serialize(&trie).unwrap(); + let other: Trie = bincode::deserialize(&bytes).unwrap(); + + assert_eq!(trie, other); + } + + mod cached { + use super::*; + use crate::CachedTrie; + use alloy_primitives::B256; + + #[test] + fn round_trip() { + let mut trie: CachedTrie = (0..N) + .map(|i| (keccak256(i.to_be_bytes()), Bytes::from(alloy_rlp::encode(i)))) + .collect(); + trie.hash(); + assert!(trie.hash.is_some()); + + let bytes = bincode::serialize(&trie).unwrap(); + let other: CachedTrie = bincode::deserialize(&bytes).unwrap(); + assert!(other.hash.is_none()); + + assert_eq!(trie, other); + } + + #[test] + fn round_trip_dup() { + let trie: CachedTrie = + (0..255).map(|i| (B256::with_last_byte(i), Bytes::from(B256::ZERO))).collect(); + + let bytes = bincode::serialize(&trie).unwrap(); + let other: CachedTrie = bincode::deserialize(&bytes).unwrap(); + + assert_eq!(trie, other); + } + } +} diff --git a/scripts/run_ef_tests.sh b/scripts/run_ef_tests.sh index 1067a03..e2de402 100755 --- a/scripts/run_ef_tests.sh +++ b/scripts/run_ef_tests.sh @@ -3,8 +3,23 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " + exit 1 +fi + +TRIE_IMPL="$1" +case "$TRIE_IMPL" in +default|zeth) ;; +*) + echo "Invalid trie implementation: $TRIE_IMPL" + echo "Expected one of: default, zeth" + exit 1 + ;; +esac + # Setup test fixtures "$SCRIPT_DIR/setup_ef_tests.sh" # Run EF tests -cargo nextest run --no-fail-fast -p ef-tests --release --features "asm-keccak ef-tests" +EF_TEST_TRIE="$TRIE_IMPL" cargo nextest run --no-fail-fast -p ef-tests --release --features "asm-keccak ef-tests" diff --git a/testing/ef-tests/Cargo.toml b/testing/ef-tests/Cargo.toml index 2a0535a..d1cee6a 100644 --- a/testing/ef-tests/Cargo.toml +++ b/testing/ef-tests/Cargo.toml @@ -17,6 +17,7 @@ asm-keccak = ["alloy-primitives/asm-keccak", "revm/asm-keccak"] [dependencies] stateless.workspace = true +tries.workspace = true reth-chainspec = { workspace = true, features = ["std"] } reth-ethereum-primitives = { workspace = true, features = ["std"] } diff --git a/testing/ef-tests/src/cases/blockchain_test.rs b/testing/ef-tests/src/cases/blockchain_test.rs index c1d3551..ea64b1d 100644 --- a/testing/ef-tests/src/cases/blockchain_test.rs +++ b/testing/ef-tests/src/cases/blockchain_test.rs @@ -26,15 +26,42 @@ use reth_revm::{State, database::StateProviderDatabase, witness::ExecutionWitnes use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot}; use reth_trie_db::DatabaseStateRoot; use stateless::{ - ExecutionWitness, UncompressedPublicKey, trie::StatelessSparseTrie, - validation::stateless_validation_with_trie, + ExecutionWitness, UncompressedPublicKey, validation::stateless_validation_with_trie, }; use std::{ collections::BTreeMap, - fs, + env, fs, path::{Path, PathBuf}, sync::Arc, }; +use tries::{StatelessTrie, default::StatelessSparseTrie, zeth::SparseState}; + +/// Environment variable used by EF tests to select the trie implementation. +const EF_TEST_TRIE_ENV_VAR: &str = "EF_TEST_TRIE"; + +#[derive(Debug, Clone, Copy)] +enum EfTestTrie { + Default, + Zeth, +} + +impl EfTestTrie { + fn from_env() -> Result { + let value = env::var(EF_TEST_TRIE_ENV_VAR).map_err(|_| { + Error::Assertion(format!( + "missing {EF_TEST_TRIE_ENV_VAR} env var; expected one of: `default`, `zeth`" + )) + })?; + + match value.as_str() { + "default" => Ok(Self::Default), + "zeth" => Ok(Self::Zeth), + _ => Err(Error::Assertion(format!( + "invalid {EF_TEST_TRIE_ENV_VAR} value `{value}`; expected `default` or `zeth`" + ))), + } + } +} /// A handler for the blockchain test suite. #[derive(Debug)] @@ -210,6 +237,18 @@ impl Case for BlockchainTestCase { fn run_case( case: &BlockchainTest, ) -> Result, ExecutionWitness)>, Error> { + match EfTestTrie::from_env()? { + EfTestTrie::Default => run_case_with_trie::(case), + EfTestTrie::Zeth => run_case_with_trie::(case), + } +} + +fn run_case_with_trie( + case: &BlockchainTest, +) -> Result, ExecutionWitness)>, Error> +where + T: StatelessTrie, +{ // Create a new test database and initialize a provider for the test case. let chain_spec = case.network.to_chain_spec(); let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); @@ -378,7 +417,7 @@ fn run_case( let public_keys = recover_signers(block.body().transactions()) .expect("Failed to recover public keys from transaction signatures"); - stateless_validation_with_trie::( + stateless_validation_with_trie::( block, public_keys, execution_witness.clone(),