From 12ac1f153ff0be850c8564f0fbce7b008e9f9adc Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 17 Oct 2023 10:28:05 +0300 Subject: [PATCH] feat(trie): account & storage proofs (#5041) --- Cargo.lock | 2 + clippy.toml | 1 + .../primitives/src/trie/hash_builder/mod.rs | 50 +- .../src/trie/hash_builder/proof_retainer.rs | 37 ++ crates/primitives/src/trie/mod.rs | 4 + crates/primitives/src/trie/nibbles.rs | 17 +- crates/primitives/src/trie/proofs.rs | 84 +++ crates/trie/Cargo.toml | 4 +- crates/trie/src/errors.rs | 17 - crates/trie/src/prefix_set/mod.rs | 9 + crates/trie/src/proof.rs | 557 +++++++++++------- crates/trie/testdata/proof-genesis.json | 41 ++ 12 files changed, 560 insertions(+), 263 deletions(-) create mode 100644 crates/primitives/src/trie/hash_builder/proof_retainer.rs create mode 100644 crates/primitives/src/trie/proofs.rs create mode 100644 crates/trie/testdata/proof-genesis.json diff --git a/Cargo.lock b/Cargo.lock index cf2be38f5..6e9343c89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6449,12 +6449,14 @@ dependencies = [ "alloy-rlp", "criterion", "derive_more", + "once_cell", "pretty_assertions", "proptest", "reth-db", "reth-interfaces", "reth-primitives", "reth-provider", + "serde_json", "thiserror", "tokio", "tokio-stream", diff --git a/clippy.toml b/clippy.toml index 5d37e5d80..1a6975cb0 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,2 @@ msrv = "1.70" +ignore-interior-mutability = ["bytes::Bytes", "reth_primitives::trie::nibbles::Nibbles"] diff --git a/crates/primitives/src/trie/hash_builder/mod.rs b/crates/primitives/src/trie/hash_builder/mod.rs index 35a41243d..ecb9e6b63 100644 --- a/crates/primitives/src/trie/hash_builder/mod.rs +++ b/crates/primitives/src/trie/hash_builder/mod.rs @@ -2,8 +2,11 @@ use super::{ nodes::{rlp_hash, BranchNode, ExtensionNode, LeafNode}, BranchNodeCompact, Nibbles, TrieMask, }; -use crate::{keccak256, proofs::EMPTY_ROOT, B256}; -use std::{collections::HashMap, fmt::Debug}; +use crate::{keccak256, proofs::EMPTY_ROOT, Bytes, B256}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::Debug, +}; mod state; pub use state::HashBuilderState; @@ -11,6 +14,9 @@ pub use state::HashBuilderState; mod value; pub use value::HashBuilderValue; +mod proof_retainer; +pub use proof_retainer::ProofRetainer; + /// A component used to construct the root hash of the trie. The primary purpose of a Hash Builder /// is to build the Merkle proof that is essential for verifying the integrity and authenticity of /// the trie's contents. It achieves this by constructing the root hash from the hashes of child @@ -47,6 +53,7 @@ pub struct HashBuilder { stored_in_database: bool, updated_branch_nodes: Option>, + proof_retainer: Option, rlp_buf: Vec, } @@ -62,6 +69,7 @@ impl From for HashBuilder { hash_masks: state.hash_masks, stored_in_database: state.stored_in_database, updated_branch_nodes: None, + proof_retainer: None, rlp_buf: Vec::with_capacity(32), } } @@ -90,6 +98,12 @@ impl HashBuilder { self } + /// Enable proof retainer for the specified target nibbles. + pub fn with_proof_retainer(mut self, targets: Vec) -> Self { + self.proof_retainer = Some(ProofRetainer::new(targets)); + self + } + /// Enables the Hash Builder to store updated branch nodes. /// /// Call [HashBuilder::split] to get the updates to branch nodes. @@ -105,6 +119,11 @@ impl HashBuilder { (self, updates.unwrap_or_default()) } + /// Take and return the proofs retained. + pub fn take_proofs(&mut self) -> BTreeMap { + self.proof_retainer.take().map(ProofRetainer::into_proofs).unwrap_or_default() + } + /// The number of total updates accrued. /// Returns `0` if [Self::with_updates] was not called. pub fn updates_len(&self) -> usize { @@ -141,13 +160,6 @@ impl HashBuilder { self.stored_in_database = stored_in_database; } - fn set_key_value>(&mut self, key: Nibbles, value: T) { - tracing::trace!(target: "trie::hash_builder", key = ?self.key, value = ?self.value, "old key/value"); - self.key = key; - self.value = value.into(); - tracing::trace!(target: "trie::hash_builder", key = ?self.key, value = ?self.value, "new key/value"); - } - /// Returns the current root hash of the trie builder. pub fn root(&mut self) -> B256 { // Clears the internal state @@ -159,6 +171,13 @@ impl HashBuilder { self.current_root() } + fn set_key_value>(&mut self, key: Nibbles, value: T) { + tracing::trace!(target: "trie::hash_builder", key = ?self.key, value = ?self.value, "old key/value"); + self.key = key; + self.value = value.into(); + tracing::trace!(target: "trie::hash_builder", key = ?self.key, value = ?self.value, "new key/value"); + } + fn current_root(&self) -> B256 { if let Some(node_ref) = self.stack.last() { if node_ref.len() == B256::len_bytes() + 1 { @@ -252,6 +271,7 @@ impl HashBuilder { self.rlp_buf.clear(); self.stack.push(leaf_node.rlp(&mut self.rlp_buf)); + self.retain_proof_from_buf(¤t); } HashBuilderValue::Hash(hash) => { tracing::debug!(target: "trie::hash_builder", ?hash, "pushing branch node hash"); @@ -281,6 +301,7 @@ impl HashBuilder { }, "extension node rlp"); self.rlp_buf.clear(); self.stack.push(extension_node.rlp(&mut self.rlp_buf)); + self.retain_proof_from_buf(¤t.slice(0, len_from)); self.resize_masks(len_from); } @@ -292,7 +313,7 @@ impl HashBuilder { // Insert branch nodes in the stack if !succeeding.is_empty() || preceding_exists { // Pushes the corresponding branch node to the stack - let children = self.push_branch_node(len); + let children = self.push_branch_node(¤t, len); // Need to store the branch node in an efficient format // outside of the hash builder self.store_branch_node(¤t, len, children); @@ -323,7 +344,7 @@ impl HashBuilder { /// Given the size of the longest common prefix, it proceeds to create a branch node /// from the state mask and existing stack state, and store its RLP to the top of the stack, /// after popping all the relevant elements from the stack. - fn push_branch_node(&mut self, len: usize) -> Vec { + fn push_branch_node(&mut self, current: &Nibbles, len: usize) -> Vec { let state_mask = self.groups[len]; let hash_mask = self.hash_masks[len]; let branch_node = BranchNode::new(&self.stack); @@ -331,6 +352,7 @@ impl HashBuilder { self.rlp_buf.clear(); let rlp = branch_node.rlp(state_mask, &mut self.rlp_buf); + self.retain_proof_from_buf(¤t.slice(0, len)); // Clears the stack from the branch node elements let first_child_idx = self.stack.len() - state_mask.count_ones() as usize; @@ -387,6 +409,12 @@ impl HashBuilder { } } + fn retain_proof_from_buf(&mut self, prefix: &Nibbles) { + if let Some(proof_retainer) = self.proof_retainer.as_mut() { + proof_retainer.retain(prefix, &self.rlp_buf) + } + } + fn update_masks(&mut self, current: &Nibbles, len_from: usize) { if len_from > 0 { let flag = TrieMask::from_nibble(current[len_from - 1]); diff --git a/crates/primitives/src/trie/hash_builder/proof_retainer.rs b/crates/primitives/src/trie/hash_builder/proof_retainer.rs new file mode 100644 index 000000000..04c23a929 --- /dev/null +++ b/crates/primitives/src/trie/hash_builder/proof_retainer.rs @@ -0,0 +1,37 @@ +use crate::{trie::Nibbles, Bytes}; +use std::collections::BTreeMap; + +/// Proof retainer is used to store proofs during merkle trie construction. +/// It is intended to be used within the [`HashBuilder`](crate::trie::HashBuilder). +#[derive(Debug)] +pub struct ProofRetainer { + /// The nibbles of the target trie keys to retain proofs for. + targets: Vec, + /// The map of retained proofs (RLP serialized trie nodes) + /// with their corresponding key in the trie. + proofs: BTreeMap, +} + +impl ProofRetainer { + /// Create new retainer with target nibbles. + pub fn new(targets: Vec) -> Self { + Self { targets, proofs: Default::default() } + } + + /// Returns `true` if the given prefix matches the retainer target. + pub fn matches(&self, prefix: &Nibbles) -> bool { + self.targets.iter().any(|target| target.starts_with(prefix)) + } + + /// Returns all collected proofs. + pub fn into_proofs(self) -> BTreeMap { + self.proofs + } + + /// Retain the proof if the key matches any of the targets. + pub fn retain(&mut self, prefix: &Nibbles, proof: &[u8]) { + if self.matches(prefix) { + self.proofs.insert(prefix.clone(), Bytes::from(proof.to_vec())); + } + } +} diff --git a/crates/primitives/src/trie/mod.rs b/crates/primitives/src/trie/mod.rs index 4252aa39b..f388fc6d1 100644 --- a/crates/primitives/src/trie/mod.rs +++ b/crates/primitives/src/trie/mod.rs @@ -8,6 +8,10 @@ pub use nodes::BranchNodeCompact; pub mod hash_builder; pub use hash_builder::HashBuilder; +/// Merkle trie proofs. +mod proofs; +pub use proofs::{AccountProof, StorageProof}; + mod mask; mod nibbles; mod storage; diff --git a/crates/primitives/src/trie/nibbles.rs b/crates/primitives/src/trie/nibbles.rs index f53c0ecc9..a6ad01b24 100644 --- a/crates/primitives/src/trie/nibbles.rs +++ b/crates/primitives/src/trie/nibbles.rs @@ -1,6 +1,6 @@ use crate::Bytes; use alloy_rlp::RlpEncodableWrapper; -use derive_more::{Deref, DerefMut, From, Index}; +use derive_more::{Deref, From, Index}; use reth_codecs::{main_codec, Compact}; use serde::{Deserialize, Serialize}; @@ -63,18 +63,7 @@ impl Compact for StoredNibblesSubKey { /// `hex_data` has its upper 4 bits set to zero and the lower 4 bits /// representing the nibble value. #[derive( - Default, - Clone, - Eq, - PartialEq, - RlpEncodableWrapper, - PartialOrd, - Ord, - Hash, - Index, - From, - Deref, - DerefMut, + Default, Clone, Eq, PartialEq, RlpEncodableWrapper, PartialOrd, Ord, Hash, Index, From, Deref, )] pub struct Nibbles { /// The inner representation of the nibble sequence. @@ -276,8 +265,6 @@ impl Nibbles { /// Extend the current nibbles with another nibbles. pub fn extend(&mut self, b: impl AsRef<[u8]>) { - // self.hex_data.extend_from_slice(b.as_ref()); - let mut bytes = self.hex_data.to_vec(); bytes.extend_from_slice(b.as_ref()); self.hex_data = bytes.into(); diff --git a/crates/primitives/src/trie/proofs.rs b/crates/primitives/src/trie/proofs.rs new file mode 100644 index 000000000..f603c07dc --- /dev/null +++ b/crates/primitives/src/trie/proofs.rs @@ -0,0 +1,84 @@ +use super::Nibbles; +use crate::{keccak256, Account, Address, Bytes, B256, U256}; + +/// The merkle proof with the relevant account info. +#[derive(PartialEq, Eq, Default, Debug)] +pub struct AccountProof { + /// The address associated with the account. + pub address: Address, + /// Account info. + pub info: Option, + /// Array of rlp-serialized merkle trie nodes which starting from the root node and + /// following the path of the hashed address as key. + pub proof: Vec, + /// The storage trie root. + pub storage_root: B256, + /// Array of storage proofs as requested. + pub storage_proofs: Vec, +} + +impl AccountProof { + /// Create new account proof entity. + pub fn new(address: Address) -> Self { + Self { address, ..Default::default() } + } + + /// Set account info, storage root and requested storage proofs. + pub fn set_account( + &mut self, + info: Account, + storage_root: B256, + storage_proofs: Vec, + ) { + self.info = Some(info); + self.storage_root = storage_root; + self.storage_proofs = storage_proofs; + } + + /// Set proof path. + pub fn set_proof(&mut self, proof: Vec) { + self.proof = proof; + } +} + +/// The merkle proof of the storage entry. +#[derive(PartialEq, Eq, Default, Debug)] +pub struct StorageProof { + /// The raw storage key. + pub key: B256, + /// The hashed storage key nibbles. + pub nibbles: Nibbles, + /// The storage value. + pub value: U256, + /// Array of rlp-serialized merkle trie nodes which starting from the storage root node and + /// following the path of the hashed storage slot as key. + pub proof: Vec, +} + +impl StorageProof { + /// Create new storage proof from the storage slot. + pub fn new(key: B256) -> Self { + let nibbles = Nibbles::unpack(keccak256(key)); + Self { key, nibbles, ..Default::default() } + } + + /// Create new storage proof from the storage slot and its pre-hashed image. + pub fn new_with_hashed(key: B256, hashed_key: B256) -> Self { + Self { key, nibbles: Nibbles::unpack(hashed_key), ..Default::default() } + } + + /// Create new storage proof from the storage slot and its pre-hashed image. + pub fn new_with_nibbles(key: B256, nibbles: Nibbles) -> Self { + Self { key, nibbles, ..Default::default() } + } + + /// Set storage value. + pub fn set_value(&mut self, value: U256) { + self.value = value; + } + + /// Set proof path. + pub fn set_proof(&mut self, proof: Vec) { + self.proof = proof; + } +} diff --git a/crates/trie/Cargo.toml b/crates/trie/Cargo.toml index 9fd45bfa1..5f654209a 100644 --- a/crates/trie/Cargo.toml +++ b/crates/trie/Cargo.toml @@ -44,8 +44,10 @@ triehash = "0.8" proptest.workspace = true tokio = { workspace = true, default-features = false, features = ["sync", "rt", "macros"] } tokio-stream.workspace = true -criterion = "0.5" +once_cell.workspace = true +serde_json.workspace = true pretty_assertions = "1.3.0" +criterion = "0.5" [features] test-utils = ["triehash"] diff --git a/crates/trie/src/errors.rs b/crates/trie/src/errors.rs index 4b8563062..6b742318a 100644 --- a/crates/trie/src/errors.rs +++ b/crates/trie/src/errors.rs @@ -1,4 +1,3 @@ -use reth_primitives::B256; use thiserror::Error; /// State root error. @@ -28,19 +27,3 @@ pub enum StorageRootError { #[error(transparent)] DB(#[from] reth_db::DatabaseError), } - -/// Proof error. -#[derive(Error, PartialEq, Eq, Clone, Debug)] -pub enum ProofError { - /// Leaf account missing - #[error( - "Expected leaf account with key greater or equal to {0:?} is missing from the database" - )] - LeafAccountMissing(B256), - /// Storage root error. - #[error(transparent)] - StorageRootError(#[from] StorageRootError), - /// Internal database error. - #[error(transparent)] - DB(#[from] reth_db::DatabaseError), -} diff --git a/crates/trie/src/prefix_set/mod.rs b/crates/trie/src/prefix_set/mod.rs index ce2823de9..7fe8cb9c1 100644 --- a/crates/trie/src/prefix_set/mod.rs +++ b/crates/trie/src/prefix_set/mod.rs @@ -37,6 +37,15 @@ pub struct PrefixSetMut { index: usize, } +impl From for PrefixSetMut +where + I: IntoIterator, +{ + fn from(value: I) -> Self { + PrefixSetMut { keys: value.into_iter().collect(), ..Default::default() } + } +} + impl PrefixSetMut { /// Returns `true` if any of the keys in the set has the given prefix or /// if the given prefix is a prefix of any key in the set. diff --git a/crates/trie/src/proof.rs b/crates/trie/src/proof.rs index 77c370bc4..a70d49172 100644 --- a/crates/trie/src/proof.rs +++ b/crates/trie/src/proof.rs @@ -1,39 +1,25 @@ use crate::{ account::EthAccount, - hashed_cursor::{HashedAccountCursor, HashedCursorFactory}, - prefix_set::PrefixSet, - trie_cursor::{AccountTrieCursor, TrieCursor}, + hashed_cursor::{HashedAccountCursor, HashedCursorFactory, HashedStorageCursor}, + prefix_set::PrefixSetMut, + trie_cursor::{AccountTrieCursor, StorageTrieCursor}, walker::TrieWalker, - ProofError, StorageRoot, + StorageRootError, }; -use alloy_rlp::Encodable; -use reth_db::{cursor::DbCursorRO, tables, transaction::DbTx}; +use alloy_rlp::{BufMut, Encodable}; +use reth_db::{tables, transaction::DbTx}; use reth_primitives::{ keccak256, - trie::{ - nodes::{rlp_hash, BranchNode, LeafNode, CHILD_INDEX_RANGE}, - BranchNodeCompact, HashBuilder, Nibbles, - }, - Address, Bytes, B256, + proofs::EMPTY_ROOT, + trie::{AccountProof, HashBuilder, Nibbles, StorageProof}, + Address, StorageEntry, B256, }; /// A struct for generating merkle proofs. /// -/// Proof generator starts with acquiring the trie walker and restoring the root node in the trie. -/// The root node is restored from its immediate children which are stored in the database. -/// -/// Upon encountering the child of the root node that matches the prefix of the requested account's -/// hashed key, the proof generator traverses the path down to the leaf node (excluded as we don't -/// store leaf nodes in the database). The proof generator stops traversing the path upon -/// encountering a branch node with no children matching the hashed key. -/// -/// After traversing the branch node path, the proof generator attempts to restore the leaf node of -/// the target account by looking up the target account info. -/// If the leaf node exists, we encoded it and add it to the proof thus proving **inclusion**. -/// If the leaf node does not exist, we return the proof as is thus proving **exclusion**. -/// -/// After traversing the path, the proof generator continues to restore the root node of the trie -/// until completion. The root node is then inserted at the start of the proof. +/// Proof generator adds the target address and slots to the prefix set, enables the proof retainer +/// on the hash builder and follows the same algorithm as the state root calculator. +/// See `StateRoot::root` for more info. #[derive(Debug)] pub struct Proof<'a, TX, H> { /// A reference to the database transaction. @@ -55,213 +41,157 @@ where H: HashedCursorFactory + Clone, { /// Generate an account proof from intermediate nodes. - pub fn account_proof(&self, address: Address) -> Result, ProofError> { - let hashed_address = keccak256(address); - let target_nibbles = Nibbles::unpack(hashed_address); + pub fn account_proof( + &self, + address: Address, + slots: &[B256], + ) -> Result { + let target_hashed_address = keccak256(address); + let target_nibbles = Nibbles::unpack(target_hashed_address); + let mut account_proof = AccountProof::new(address); - let mut proof_restorer = ProofRestorer::new(self.tx)? - .with_hashed_cursor_factory(self.hashed_cursor_factory.clone())?; let mut trie_cursor = AccountTrieCursor::new(self.tx.cursor_read::()?); + let mut hashed_account_cursor = self.hashed_cursor_factory.hashed_account_cursor()?; - // Create the walker and immediately advance it from the root key. - let mut walker = TrieWalker::new(&mut trie_cursor, PrefixSet::default()); - walker.advance()?; + // Create the walker. + let mut prefix_set = PrefixSetMut::default(); + prefix_set.insert(target_nibbles.clone()); + let mut walker = TrieWalker::new(&mut trie_cursor, prefix_set.freeze()); // Create a hash builder to rebuild the root node since it is not available in the database. - let mut root_node_hash_builder = HashBuilder::default(); + let mut hash_builder = + HashBuilder::default().with_proof_retainer(Vec::from([target_nibbles.clone()])); - let mut proofs: Vec = Vec::new(); + let mut account_rlp = Vec::with_capacity(128); while let Some(key) = walker.key() { - if target_nibbles.has_prefix(&key) { - debug_assert!(proofs.is_empty(), "Prefix must match a single key"); - proofs = self.traverse_path(walker.cursor, &mut proof_restorer, hashed_address)?; + if walker.can_skip_current_node { + let value = walker.hash().unwrap(); + let is_in_db_trie = walker.children_are_in_trie(); + hash_builder.add_branch(key.clone(), value, is_in_db_trie); } - let value = walker.hash().unwrap(); - let is_in_db_trie = walker.children_are_in_trie(); - root_node_hash_builder.add_branch(key.clone(), value, is_in_db_trie); - walker.advance()?; + let seek_key = match walker.next_unprocessed_key() { + Some(key) => key, + None => break, // no more keys + }; + + let next_key = walker.advance()?; + let mut next_account_entry = hashed_account_cursor.seek(seek_key)?; + while let Some((hashed_address, account)) = next_account_entry { + let account_nibbles = Nibbles::unpack(hashed_address); + + if let Some(ref key) = next_key { + if key < &account_nibbles { + break + } + } + + let storage_root = if hashed_address == target_hashed_address { + let (storage_root, storage_proofs) = + self.storage_root_with_proofs(hashed_address, slots)?; + account_proof.set_account(account, storage_root, storage_proofs); + storage_root + } else { + self.storage_root(hashed_address)? + }; + + account_rlp.clear(); + let account = EthAccount::from(account).with_storage_root(storage_root); + account.encode(&mut &mut account_rlp as &mut dyn BufMut); + + hash_builder.add_leaf(account_nibbles, &account_rlp); + + // Move the next account entry + next_account_entry = hashed_account_cursor.next()?; + } } - // TODO: This is a hack to retrieve the root node from the hash builder. - // We should find a better way. - root_node_hash_builder.set_updates(true); - let _ = root_node_hash_builder.root(); - let (_, mut updates) = root_node_hash_builder.split(); - let root_node = updates.remove(&Nibbles::default()).expect("root node is present"); + let _ = hash_builder.root(); - // Restore the root node RLP and prepend it to the proofs result - let root_node_rlp = proof_restorer.restore_branch_node(&Nibbles::default(), root_node)?; - proofs.insert(0, root_node_rlp); + let proofs = hash_builder.take_proofs(); + account_proof.set_proof(proofs.values().cloned().collect()); - Ok(proofs) + Ok(account_proof) } - fn traverse_path>( + /// Compute storage root. + pub fn storage_root(&self, hashed_address: B256) -> Result { + let (storage_root, _) = self.storage_root_with_proofs(hashed_address, &[])?; + Ok(storage_root) + } + + /// Compute the storage root and retain proofs for requested slots. + pub fn storage_root_with_proofs( &self, - trie_cursor: &mut AccountTrieCursor, - proof_restorer: &mut ProofRestorer<'a, TX, H>, hashed_address: B256, - ) -> Result, ProofError> { - let mut intermediate_proofs = Vec::new(); + slots: &[B256], + ) -> Result<(B256, Vec), StorageRootError> { + let mut hashed_storage_cursor = self.hashed_cursor_factory.hashed_storage_cursor()?; - let target = Nibbles::unpack(hashed_address); - let mut current_prefix = target.slice(0, 1); - while let Some((_, node)) = - trie_cursor.seek_exact(current_prefix.hex_data.to_vec().into())? - { - let branch_node_rlp = proof_restorer.restore_branch_node(¤t_prefix, node)?; - intermediate_proofs.push(branch_node_rlp); + let mut trie_cursor = StorageTrieCursor::new( + self.tx.cursor_dup_read::()?, + hashed_address, + ); - if current_prefix.len() < target.len() { - current_prefix.extend([target.0[current_prefix.len()]]); + let mut proofs = slots.iter().copied().map(StorageProof::new).collect::>(); + + // short circuit on empty storage + if hashed_storage_cursor.is_storage_empty(hashed_address)? { + return Ok((EMPTY_ROOT, proofs)) + } + + let target_nibbles = proofs.iter().map(|p| p.nibbles.clone()).collect::>(); + let prefix_set = PrefixSetMut::from(target_nibbles.clone()).freeze(); + let mut walker = TrieWalker::new(&mut trie_cursor, prefix_set); + + let mut hash_builder = HashBuilder::default().with_proof_retainer(target_nibbles); + while let Some(key) = walker.key() { + if walker.can_skip_current_node { + hash_builder.add_branch(key, walker.hash().unwrap(), walker.children_are_in_trie()); + } + + let seek_key = match walker.next_unprocessed_key() { + Some(key) => key, + None => break, // no more keys + }; + + let next_key = walker.advance()?; + let mut storage = hashed_storage_cursor.seek(hashed_address, seek_key)?; + while let Some(StorageEntry { key: hashed_key, value }) = storage { + let hashed_key_nibbles = Nibbles::unpack(hashed_key); + if let Some(ref key) = next_key { + if key < &hashed_key_nibbles { + break + } + } + + if let Some(proof) = + proofs.iter_mut().find(|proof| proof.nibbles == hashed_key_nibbles) + { + proof.set_value(value); + } + + hash_builder + .add_leaf(hashed_key_nibbles, alloy_rlp::encode_fixed_size(&value).as_ref()); + storage = hashed_storage_cursor.next()?; } } - if let Some(leaf_node_rlp) = - proof_restorer.restore_target_leaf_node(hashed_address, current_prefix.len())? - { - intermediate_proofs.push(leaf_node_rlp); + let root = hash_builder.root(); + + let proof_nodes = hash_builder.take_proofs(); + for proof in proofs.iter_mut() { + proof.set_proof( + proof_nodes + .iter() + .filter(|(path, _)| proof.nibbles.starts_with(path)) + .map(|(_, node)| node.clone()) + .collect(), + ); } - Ok(intermediate_proofs) - } -} - -struct ProofRestorer<'a, TX, H> -where - H: HashedCursorFactory, -{ - /// A reference to the database transaction. - tx: &'a TX, - /// The factory for hashed cursors. - hashed_cursor_factory: H, - /// The hashed account cursor. - hashed_account_cursor: H::AccountCursor, - /// Pre-allocated buffer for account RLP encoding - account_rlp_buf: Vec, - /// Pre-allocated buffer for branch/leaf node RLP encoding - node_rlp_buf: Vec, -} - -impl<'a, 'tx, TX> ProofRestorer<'a, TX, &'a TX> -where - TX: DbTx<'tx>, -{ - fn new(tx: &'a TX) -> Result { - let hashed_account_cursor = tx.hashed_account_cursor()?; - Ok(Self { - tx, - hashed_cursor_factory: tx, - hashed_account_cursor, - account_rlp_buf: Vec::with_capacity(128), - node_rlp_buf: Vec::with_capacity(128), - }) - } -} - -impl<'a, 'tx, TX, H> ProofRestorer<'a, TX, H> -where - TX: DbTx<'tx>, - H: HashedCursorFactory + Clone, -{ - /// Set the hashed cursor factory. - fn with_hashed_cursor_factory( - self, - hashed_cursor_factory: HF, - ) -> Result, ProofError> - where - HF: HashedCursorFactory, - { - let hashed_account_cursor = hashed_cursor_factory.hashed_account_cursor()?; - Ok(ProofRestorer { - tx: self.tx, - hashed_cursor_factory, - hashed_account_cursor, - account_rlp_buf: self.account_rlp_buf, - node_rlp_buf: self.node_rlp_buf, - }) - } - - fn restore_branch_node( - &mut self, - prefix: &Nibbles, - node: BranchNodeCompact, - ) -> Result { - let mut hash_idx = 0; - let mut branch_node_stack = Vec::with_capacity(node.state_mask.count_ones() as usize); - - for child in CHILD_INDEX_RANGE.filter(|ch| node.state_mask.is_bit_set(*ch)) { - if node.hash_mask.is_bit_set(child) { - branch_node_stack.push(rlp_hash(node.hashes[hash_idx])); - hash_idx += 1; - } else { - let child_key = prefix.join(&Nibbles::from_hex(Vec::from([child]))); - let mut child_key_to_seek = child_key.pack(); - child_key_to_seek.resize(32, 0); - - let leaf_node_rlp = - self.restore_leaf_node(B256::from_slice(&child_key_to_seek), child_key.len())?; - branch_node_stack.push(leaf_node_rlp.to_vec()); - } - } - - self.node_rlp_buf.clear(); - BranchNode::new(&branch_node_stack).rlp(node.state_mask, &mut self.node_rlp_buf); - Ok(Bytes::copy_from_slice(self.node_rlp_buf.as_slice())) - } - - /// Restore leaf node. - /// The leaf nodes are always encoded as `RLP(node) or RLP(keccak(RLP(node)))`. - fn restore_leaf_node(&mut self, seek_key: B256, slice_at: usize) -> Result { - let (hashed_address, account) = self - .hashed_account_cursor - .seek(seek_key)? - .ok_or(ProofError::LeafAccountMissing(seek_key))?; - - // Restore account's storage root. - let storage_root = StorageRoot::new_hashed(self.tx, hashed_address) - .with_hashed_cursor_factory(self.hashed_cursor_factory.clone()) - .root()?; - - self.account_rlp_buf.clear(); - EthAccount::from(account).with_storage_root(storage_root).encode(&mut self.account_rlp_buf); - - let leaf_node_key = Nibbles::unpack(hashed_address).slice_from(slice_at); - let leaf_node = LeafNode::new(&leaf_node_key, &self.account_rlp_buf); - - self.node_rlp_buf.clear(); - Ok(Bytes::from(leaf_node.rlp(&mut self.node_rlp_buf))) - } - - /// Restore target leaf node. - /// The target node has to have an exactly matching key and is always encoded as `RLP(node)`. - /// The target node might be missing from the trie. - fn restore_target_leaf_node( - &mut self, - seek_key: B256, - slice_at: usize, - ) -> Result, ProofError> { - let (hashed_address, account) = match self.hashed_account_cursor.seek(seek_key)? { - Some(entry) if entry.0 == seek_key => entry, - _ => return Ok(None), - }; - - // Restore account's storage root. - let storage_root = StorageRoot::new_hashed(self.tx, hashed_address) - .with_hashed_cursor_factory(self.hashed_cursor_factory.clone()) - .root()?; - - self.account_rlp_buf.clear(); - EthAccount::from(account).with_storage_root(storage_root).encode(&mut self.account_rlp_buf); - - let leaf_node_key = Nibbles::unpack(hashed_address).slice_from(slice_at); - let leaf_node = LeafNode::new(&leaf_node_key, &self.account_rlp_buf); - - self.node_rlp_buf.clear(); - leaf_node.rlp(&mut self.node_rlp_buf); - Ok(Some(Bytes::copy_from_slice(self.node_rlp_buf.as_slice()))) + Ok((root, proofs)) } } @@ -269,12 +199,38 @@ where mod tests { use super::*; use crate::StateRoot; + use once_cell::sync::Lazy; use reth_db::{database::Database, test_utils::create_test_rw_db}; use reth_interfaces::RethResult; - use reth_primitives::{ChainSpec, StorageEntry, MAINNET}; + use reth_primitives::{Account, Bytes, Chain, ChainSpec, StorageEntry, HOLESKY, MAINNET, U256}; use reth_provider::{HashingWriter, ProviderFactory}; use std::{str::FromStr, sync::Arc}; + /* + World State (sampled from ) + | address | prefix | hash | balance + |--------------------------------------------|-----------|--------------------------------------------------------------------|-------- + | 0x2031f89b3ea8014eb51a78c316e42af3e0d7695f | 0xa711355 | 0xa711355ec1c8f7e26bb3ccbcb0b75d870d15846c0b98e5cc452db46c37faea40 | 45 eth + | 0x33f0fc440b8477fcfbe9d0bf8649e7dea9baedb2 | 0xa77d337 | 0xa77d337781e762f3577784bab7491fcc43e291ce5a356b9bc517ac52eed3a37a | 1 wei + | 0x62b0dd4aab2b1a0a04e279e2b828791a10755528 | 0xa7f9365 | 0xa7f936599f93b769acf90c7178fd2ddcac1b5b4bc9949ee5a04b7e0823c2446e | 1.1 eth + | 0x1ed9b1dd266b607ee278726d324b855a093394a6 | 0xa77d397 | 0xa77d397a32b8ab5eb4b043c65b1f00c93f517bc8883c5cd31baf8e8a279475e3 | .12 eth + + All expected testspec results were obtained from querying proof RPC on the running geth instance `geth init crates/trie/testdata/proof-genesis.json && geth --http`. + */ + static TEST_SPEC: Lazy> = Lazy::new(|| { + ChainSpec { + chain: Chain::Id(12345), + genesis: serde_json::from_str(include_str!("../testdata/proof-genesis.json")) + .expect("Can't deserialize test genesis json"), + ..Default::default() + } + .into() + }); + + fn convert_to_proof<'a>(path: impl IntoIterator) -> Vec { + path.into_iter().map(Bytes::from_str).collect::, _>>().unwrap() + } + fn insert_genesis(db: DB, chain_spec: Arc) -> RethResult<()> { let provider_factory = ProviderFactory::new(db, chain_spec.clone()); let mut provider = provider_factory.provider_rw()?; @@ -309,7 +265,84 @@ mod tests { } #[test] - fn genesis_account_proof() { + fn testspec_proofs() { + // Create test database and insert genesis accounts. + let db = create_test_rw_db(); + insert_genesis(db.clone(), TEST_SPEC.clone()).unwrap(); + + let tx = db.tx().unwrap(); + + let data = Vec::from([ + ( + "0x2031f89b3ea8014eb51a78c316e42af3e0d7695f", + convert_to_proof([ + "0xe48200a7a040f916999be583c572cc4dd369ec53b0a99f7de95f13880cf203d98f935ed1b3", + "0xf87180a04fb9bab4bb88c062f32452b7c94c8f64d07b5851d44a39f1e32ba4b1829fdbfb8080808080a0b61eeb2eb82808b73c4ad14140a2836689f4ab8445d69dd40554eaf1fce34bc080808080808080a0dea230ff2026e65de419288183a340125b04b8405cc61627b3b4137e2260a1e880", + "0xf8719f31355ec1c8f7e26bb3ccbcb0b75d870d15846c0b98e5cc452db46c37faea40b84ff84d80890270801d946c940000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ]) + ), + ( + "0x33f0fc440b8477fcfbe9d0bf8649e7dea9baedb2", + convert_to_proof([ + "0xe48200a7a040f916999be583c572cc4dd369ec53b0a99f7de95f13880cf203d98f935ed1b3", + "0xf87180a04fb9bab4bb88c062f32452b7c94c8f64d07b5851d44a39f1e32ba4b1829fdbfb8080808080a0b61eeb2eb82808b73c4ad14140a2836689f4ab8445d69dd40554eaf1fce34bc080808080808080a0dea230ff2026e65de419288183a340125b04b8405cc61627b3b4137e2260a1e880", + "0xe48200d3a0ef957210bca5b9b402d614eb8408c88cfbf4913eb6ab83ca233c8b8f0e626b54", + "0xf851808080a02743a5addaf4cf9b8c0c073e1eaa555deaaf8c41cb2b41958e88624fa45c2d908080808080a0bfbf6937911dfb88113fecdaa6bde822e4e99dae62489fcf61a91cb2f36793d680808080808080", + "0xf8679e207781e762f3577784bab7491fcc43e291ce5a356b9bc517ac52eed3a37ab846f8448001a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ]) + ), + ( + "0x62b0dd4aab2b1a0a04e279e2b828791a10755528", + convert_to_proof([ + "0xe48200a7a040f916999be583c572cc4dd369ec53b0a99f7de95f13880cf203d98f935ed1b3", + "0xf87180a04fb9bab4bb88c062f32452b7c94c8f64d07b5851d44a39f1e32ba4b1829fdbfb8080808080a0b61eeb2eb82808b73c4ad14140a2836689f4ab8445d69dd40554eaf1fce34bc080808080808080a0dea230ff2026e65de419288183a340125b04b8405cc61627b3b4137e2260a1e880", + "0xf8709f3936599f93b769acf90c7178fd2ddcac1b5b4bc9949ee5a04b7e0823c2446eb84ef84c80880f43fc2c04ee0000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ]) + ), + ( + "0x1ed9b1dd266b607ee278726d324b855a093394a6", + convert_to_proof([ + "0xe48200a7a040f916999be583c572cc4dd369ec53b0a99f7de95f13880cf203d98f935ed1b3", + "0xf87180a04fb9bab4bb88c062f32452b7c94c8f64d07b5851d44a39f1e32ba4b1829fdbfb8080808080a0b61eeb2eb82808b73c4ad14140a2836689f4ab8445d69dd40554eaf1fce34bc080808080808080a0dea230ff2026e65de419288183a340125b04b8405cc61627b3b4137e2260a1e880", + "0xe48200d3a0ef957210bca5b9b402d614eb8408c88cfbf4913eb6ab83ca233c8b8f0e626b54", + "0xf851808080a02743a5addaf4cf9b8c0c073e1eaa555deaaf8c41cb2b41958e88624fa45c2d908080808080a0bfbf6937911dfb88113fecdaa6bde822e4e99dae62489fcf61a91cb2f36793d680808080808080", + "0xf86f9e207a32b8ab5eb4b043c65b1f00c93f517bc8883c5cd31baf8e8a279475e3b84ef84c808801aa535d3d0c0000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ]) + ), + ]); + + for (target, expected_proof) in data { + let target = Address::from_str(target).unwrap(); + let account_proof = Proof::new(&tx).account_proof(target, &[]).unwrap(); + pretty_assertions::assert_eq!( + account_proof.proof, + expected_proof, + "proof for {target:?} does not match" + ); + } + } + + #[test] + fn testspec_empty_storage_proof() { + // Create test database and insert genesis accounts. + let db = create_test_rw_db(); + insert_genesis(db.clone(), TEST_SPEC.clone()).unwrap(); + + let tx = db.tx().unwrap(); + + let target = Address::from_str("0x1ed9b1dd266b607ee278726d324b855a093394a6").unwrap(); + let slots = Vec::from([B256::with_last_byte(1), B256::with_last_byte(3)]); + let account_proof = Proof::new(&tx).account_proof(target, &slots).unwrap(); + assert_eq!(account_proof.storage_root, EMPTY_ROOT, "expected empty storage root"); + + assert_eq!(slots.len(), account_proof.storage_proofs.len()); + for (idx, slot) in slots.into_iter().enumerate() { + assert_eq!(account_proof.storage_proofs.get(idx), Some(&StorageProof::new(slot))); + } + } + + #[test] + fn mainnet_genesis_account_proof() { // Create test database and insert genesis accounts. let db = create_test_rw_db(); insert_genesis(db.clone(), MAINNET.clone()).unwrap(); @@ -319,21 +352,21 @@ mod tests { let target = Address::from_str("0x000d836201318ec6899a67540690382780743280").unwrap(); // `cast proof 0x000d836201318ec6899a67540690382780743280 --block 0` - let expected_account_proof = [ + let expected_account_proof = convert_to_proof([ "0xf90211a090dcaf88c40c7bbc95a912cbdde67c175767b31173df9ee4b0d733bfdd511c43a0babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bda0473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021a0bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0a04e44caecff45c9891f74f6a2156735886eedf6f1a733628ebc802ec79d844648a0a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7a0e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92da0f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721a07117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681a069eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472a0203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8ea09287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208a06fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94eca07b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475a051f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0a089d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb80", "0xf90211a0dae48f5b47930c28bb116fbd55e52cd47242c71bf55373b55eb2805ee2e4a929a00f1f37f337ec800e2e5974e2e7355f10f1a4832b39b846d916c3597a460e0676a0da8f627bb8fbeead17b318e0a8e4f528db310f591bb6ab2deda4a9f7ca902ab5a0971c662648d58295d0d0aa4b8055588da0037619951217c22052802549d94a2fa0ccc701efe4b3413fd6a61a6c9f40e955af774649a8d9fd212d046a5a39ddbb67a0d607cdb32e2bd635ee7f2f9e07bc94ddbd09b10ec0901b66628e15667aec570ba05b89203dc940e6fa70ec19ad4e01d01849d3a5baa0a8f9c0525256ed490b159fa0b84227d48df68aecc772939a59afa9e1a4ab578f7b698bdb1289e29b6044668ea0fd1c992070b94ace57e48cbf6511a16aa770c645f9f5efba87bbe59d0a042913a0e16a7ccea6748ae90de92f8aef3b3dc248a557b9ac4e296934313f24f7fced5fa042373cf4a00630d94de90d0a23b8f38ced6b0f7cb818b8925fee8f0c2a28a25aa05f89d2161c1741ff428864f7889866484cef622de5023a46e795dfdec336319fa07597a017664526c8c795ce1da27b8b72455c49657113e0455552dbc068c5ba31a0d5be9089012fda2c585a1b961e988ea5efcd3a06988e150a8682091f694b37c5a0f7b0352e38c315b2d9a14d51baea4ddee1770974c806e209355233c3c89dce6ea049bf6e8df0acafd0eff86defeeb305568e44d52d2235cf340ae15c6034e2b24180", "0xf901f1a0cf67e0f5d5f8d70e53a6278056a14ddca46846f5ef69c7bde6810d058d4a9eda80a06732ada65afd192197fe7ce57792a7f25d26978e64e954b7b84a1f7857ac279da05439f8d011683a6fc07efb90afca198fd7270c795c835c7c85d91402cda992eaa0449b93033b6152d289045fdb0bf3f44926f831566faa0e616b7be1abaad2cb2da031be6c3752bcd7afb99b1bb102baf200f8567c394d464315323a363697646616a0a40e3ed11d906749aa501279392ffde868bd35102db41364d9c601fd651f974aa0044bfa4fe8dd1a58e6c7144da79326e94d1331c0b00373f6ae7f3662f45534b7a098005e3e48db68cb1dc9b9f034ff74d2392028ddf718b0f2084133017da2c2e7a02a62bc40414ee95b02e202a9e89babbabd24bef0abc3fc6dcd3e9144ceb0b725a0239facd895bbf092830390a8676f34b35b29792ae561f196f86614e0448a5792a0a4080f88925daff6b4ce26d188428841bd65655d8e93509f2106020e76d41eefa04918987904be42a6894256ca60203283d1b89139cf21f09f5719c44b8cdbb8f7a06201fc3ef0827e594d953b5e3165520af4fceb719e11cc95fd8d3481519bfd8ca05d0e353d596bd725b09de49c01ede0f29023f0153d7b6d401556aeb525b2959ba0cd367d0679950e9c5f2aa4298fd4b081ade2ea429d71ff390c50f8520e16e30880", "0xf87180808080808080a0dbee8b33c73b86df839f309f7ac92eee19836e08b39302ffa33921b3c6a09f66a06068b283d51aeeee682b8fb5458354315d0b91737441ede5e137c18b4775174a8080808080a0fe7779c7d58c2fda43eba0a6644043c86ebb9ceb4836f89e30831f23eb059ece8080", "0xf8719f20b71c90b0d523dd5004cf206f325748da347685071b34812e21801f5270c4b84ff84d80890ad78ebc5ac6200000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" - ].into_iter().map(Bytes::from_str).collect::, _>>().unwrap(); + ]); let tx = db.tx().unwrap(); - let proof = Proof::new(&tx).account_proof(target).unwrap(); - pretty_assertions::assert_eq!(proof, expected_account_proof); + let account_proof = Proof::new(&tx).account_proof(target, &[]).unwrap(); + pretty_assertions::assert_eq!(account_proof.proof, expected_account_proof); } #[test] - fn genesis_account_proof_nonexistent() { + fn mainnet_genesis_account_proof_nonexistent() { // Create test database and insert genesis accounts. let db = create_test_rw_db(); insert_genesis(db.clone(), MAINNET.clone()).unwrap(); @@ -343,14 +376,100 @@ mod tests { let target = Address::from_str("0x000d836201318ec6899a67540690382780743281").unwrap(); // `cast proof 0x000d836201318ec6899a67540690382780743281 --block 0` - let expected_account_proof = [ + let expected_account_proof = convert_to_proof([ "0xf90211a090dcaf88c40c7bbc95a912cbdde67c175767b31173df9ee4b0d733bfdd511c43a0babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bda0473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021a0bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0a04e44caecff45c9891f74f6a2156735886eedf6f1a733628ebc802ec79d844648a0a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7a0e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92da0f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721a07117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681a069eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472a0203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8ea09287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208a06fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94eca07b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475a051f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0a089d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb80", "0xf90211a0586b1ddec8db4824154209d355a1989b6c43aa69aba36e9d70c9faa53e7452baa0f86db47d628c73764d74b9ccaed73b8486d97a7731d57008fc9efaf417411860a0d9faed7b9ea107b5d98524246c977e782377f976e34f70717e8b1207f2f9b981a00218f59ccedf797c95e27c56405b9bf16845050fb43e773b66b26bc6992744f5a0dbf396f480c4e024156644adea7c331688d03742369e9d87ab8913bc439ff975a0aced524f39b22c62a5be512ddbca89f0b89b47c311065ccf423dee7013c7ea83a0c06b05f80b237b403adc019c0bc95b5de935021b14a75cbc18509eec60dfd83aa085339d45c4a52b7d523c301701f1ab339964e9c907440cff0a871c98dcf8811ea03ae9f6b8e227ec9be9461f0947b01696f78524c4519a6dee9fba14d209952cf9a0af17f551f9fa1ba4be41d0b342b160e2e8468d7e98a65a2dbf9d5fe5d6928024a0b850ac3bc03e9a309cc59ce5f1ab8db264870a7a22786081753d1db91897b8e6a09e796a4904bd78cb2655b5f346c94350e2d5f0dbf2bc00ac00871cd7ba46b241a0f6f0377427b900529caf32abf32ba1eb93f5f70153aa50b90bf55319a434c252a0725eaf27c8ee07e9b2511a6d6a0d71c649d855e8a9ed26e667903e2e94ae47cba0e4139fb48aa1a524d47f6e0df80314b88b52202d7e853da33c276aa8572283a8a05e9003d54a45935fdebae3513dc7cd16626dc05e1d903ae7f47f1a35aa6e234580", "0xf901d1a0b7c55b381eb205712a2f5d1b7d6309ac725da79ab159cb77dc2783af36e6596da0b3b48aa390e0f3718b486ccc32b01682f92819e652315c1629058cd4d9bb1545a0e3c0cc68af371009f14416c27e17f05f4f696566d2ba45362ce5711d4a01d0e4a0bad1e085e431b510508e2a9e3712633a414b3fe6fd358635ab206021254c1e10a0f8407fe8d5f557b9e012d52e688139bd932fec40d48630d7ff4204d27f8cc68da08c6ca46eff14ad4950e65469c394ca9d6b8690513b1c1a6f91523af00082474c80a0630c034178cb1290d4d906edf28688804d79d5e37a3122c909adab19ac7dc8c5a059f6d047c5d1cc75228c4517a537763cb410c38554f273e5448a53bc3c7166e7a0d842f53ce70c3aad1e616fa6485d3880d15c936fcc306ec14ae35236e5a60549a0218ee2ee673c69b4e1b953194b2568157a69085b86e4f01644fa06ab472c6cf9a016a35a660ea496df7c0da646378bfaa9562f401e42a5c2fe770b7bbe22433585a0dd0fbbe227a4d50868cdbb3107573910fd97131ea8d835bef81d91a2fc30b175a06aafa3d78cf179bf055bd5ec629be0ff8352ce0aec9125a4d75be3ee7eb71f10a01d6817ef9f64fcbb776ff6df0c83138dcd2001bd752727af3e60f4afc123d8d58080" - ].into_iter().map(Bytes::from_str).collect::, _>>().unwrap(); + ]); let tx = db.tx().unwrap(); - let proof = Proof::new(&tx).account_proof(target).unwrap(); - pretty_assertions::assert_eq!(proof, expected_account_proof); + let account_proof = Proof::new(&tx).account_proof(target, &[]).unwrap(); + pretty_assertions::assert_eq!(account_proof.proof, expected_account_proof); + } + + #[test] + fn holesky_deposit_contract_proof() { + // Create test database and insert genesis accounts. + let db = create_test_rw_db(); + insert_genesis(db.clone(), HOLESKY.clone()).unwrap(); + + let tx = db.tx().unwrap(); + + let target = Address::from_str("0x4242424242424242424242424242424242424242").unwrap(); + // existent + let slot_22 = + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000022") + .unwrap(); + let slot_23 = + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000023") + .unwrap(); + let slot_24 = + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000024") + .unwrap(); + // non-existent + let slot_100 = + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000100") + .unwrap(); + let slots = Vec::from([slot_22, slot_23, slot_24, slot_100]); + + // `cast proof 0x4242424242424242424242424242424242424242 0x22 0x23 0x24 0x100 --block 0` + let expected = AccountProof { + address: target, + info: Some(Account { + balance: U256::ZERO, + nonce: 0, + bytecode_hash: Some(B256::from_str("0x2034f79e0e33b0ae6bef948532021baceb116adf2616478703bec6b17329f1cc").unwrap()) + }), + storage_root: B256::from_str("0x556a482068355939c95a3412bdb21213a301483edb1b64402fb66ac9f3583599").unwrap(), + proof: convert_to_proof([ + "0xf90211a0ea92fb71507739d5afe328d607b2c5e98322b7aa7cdfeccf817543058b54af70a0bd0c2525b5bee47abf7120c9e01ec3249699d687f80ebb96ed9ad9de913dbab0a0ab4b14b89416eb23c6b64204fa45cfcb39d4220016a9cd0815ebb751fe45eb71a0986ae29c2148b9e61f9a7543f44a1f8d029f1c5095b359652e9ec94e64b5d393a0555d54aa23ed990b0488153418637df7b2c878b604eb761aa2673b609937b0eba0140afb6a3909cc6047b3d44af13fc83f161a7e4c4ddba430a2841862912eb222a031b1185c1f455022d9e42ce04a71f174eb9441b1ada67449510500f4d85b3b22a051ecd01e18113b23cc65e62f67d69b33ee15d20bf81a6b524f7df90ded00ca15a0703769d6a7befad000bc2b4faae3e41b809b1b1241fe2964262554e7e3603488a0e5de7f600e4e6c3c3e5630e0c66f50506a17c9715642fccb63667e81397bbf93a095f783cd1d464a60e3c8adcadc28c6eb9fec7306664df39553be41dccc909606a04225fda3b89f0c59bf40129d1d5e5c3bf67a2129f0c55e53ffdd2cebf185d644a078e0f7fd3ae5a9bc90f66169614211b48fe235eb64818b3935d3e69c53523b9aa0a870e00e53ebaa1e9ec16e5f36606fd7d21d3a3c96894c0a2a23550949d4fdf7a0809226b69cee1f4f22ced1974e7805230da1909036a49a7652428999431afac2a0f11593b2407e86e11997325d8df2d22d937bbe0aef8302ba40c6be0601b04fc380", + "0xf901f1a09da7d9755fe0c558b3c3de9fdcdf9f28ae641f38c9787b05b73ab22ae53af3e2a0d9990bf0b810d1145ecb2b011fd68c63cc85564e6724166fd4a9520180706e5fa05f5f09855df46330aa310e8d6be5fb82d1a4b975782d9b29acf06ac8d3e72b1ca0ca976997ddaf06f18992f6207e4f6a05979d07acead96568058789017cc6d06ba04d78166b48044fdc28ed22d2fd39c8df6f8aaa04cb71d3a17286856f6893ff83a004f8c7cc4f1335182a1709fb28fc67d52e59878480210abcba864d5d1fd4a066a0fc3b71c33e2e6b77c5e494c1db7fdbb447473f003daf378c7a63ba9bf3f0049d80a07b8e7a21c1178d28074f157b50fca85ee25c12568ff8e9706dcbcdacb77bf854a0973274526811393ea0bf4811ca9077531db00d06b86237a2ecd683f55ba4bcb0a03a93d726d7487874e51b52d8d534c63aa2a689df18e3b307c0d6cb0a388b00f3a06aa67101d011d1c22fe739ef83b04b5214a3e2f8e1a2625d8bfdb116b447e86fa02dd545b33c62d33a183e127a08a4767fba891d9f3b94fc20a2ca02600d6d1fffa0f3b039a4f32349e85c782d1164c1890e5bf16badc9ee4cf827db6afd2229dde6a0d9240a9d2d5851d05a97ff3305334dfdb0101e1e321fc279d2bb3cad6afa8fc8a01b69c6ab5173de8a8ec53a6ebba965713a4cc7feb86cb3e230def37c230ca2b280", + "0xf869a0202a47fc6863b89a6b51890ef3c1550d560886c027141d2058ba1e2d4c66d99ab846f8448080a0556a482068355939c95a3412bdb21213a301483edb1b64402fb66ac9f3583599a02034f79e0e33b0ae6bef948532021baceb116adf2616478703bec6b17329f1cc" + ]), + storage_proofs: Vec::from([ + StorageProof { + key: slot_22, + nibbles: Nibbles::unpack(keccak256(slot_22)), + value: U256::from_str("0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b").unwrap(), + proof: convert_to_proof([ + "0xf9019180a0aafd5b14a6edacd149e110ba6776a654f2dbffca340902be933d011113f2750380a0a502c93b1918c4c6534d4593ae03a5a23fa10ebc30ffb7080b297bff2446e42da02eb2bf45fd443bd1df8b6f9c09726a4c6252a0f7896a131a081e39a7f644b38980a0a9cf7f673a0bce76fd40332afe8601542910b48dea44e93933a3e5e930da5d19a0ddf79db0a36d0c8134ba143bcb541cd4795a9a2bae8aca0ba24b8d8963c2a77da0b973ec0f48f710bf79f63688485755cbe87f9d4c68326bb83c26af620802a80ea0f0855349af6bf84afc8bca2eda31c8ef8c5139be1929eeb3da4ba6b68a818cb0a0c271e189aeeb1db5d59d7fe87d7d6327bbe7cfa389619016459196497de3ccdea0e7503ba5799e77aa31bbe1310c312ca17b2c5bcc8fa38f266675e8f154c2516ba09278b846696d37213ab9d20a5eb42b03db3173ce490a2ef3b2f3b3600579fc63a0e9041059114f9c910adeca12dbba1fef79b2e2c8899f2d7213cd22dfe4310561a047c59da56bb2bf348c9dd2a2e8f5538a92b904b661cfe54a4298b85868bbe4858080", + "0xf85180a0776aa456ba9c5008e03b82b841a9cf2fc1e8578cfacd5c9015804eae315f17fb80808080808080808080808080a072e3e284d47badbb0a5ca1421e1179d3ea90cc10785b26b74fb8a81f0f9e841880", + "0xf843a020035b26e3e9eee00e0d72fd1ee8ddca6894550dca6916ea2ac6baa90d11e510a1a0f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" + ]) + }, + StorageProof { + key: slot_23, + nibbles: Nibbles::unpack(keccak256(slot_23)), + value: U256::from_str("0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71").unwrap(), + proof: convert_to_proof([ + "0xf9019180a0aafd5b14a6edacd149e110ba6776a654f2dbffca340902be933d011113f2750380a0a502c93b1918c4c6534d4593ae03a5a23fa10ebc30ffb7080b297bff2446e42da02eb2bf45fd443bd1df8b6f9c09726a4c6252a0f7896a131a081e39a7f644b38980a0a9cf7f673a0bce76fd40332afe8601542910b48dea44e93933a3e5e930da5d19a0ddf79db0a36d0c8134ba143bcb541cd4795a9a2bae8aca0ba24b8d8963c2a77da0b973ec0f48f710bf79f63688485755cbe87f9d4c68326bb83c26af620802a80ea0f0855349af6bf84afc8bca2eda31c8ef8c5139be1929eeb3da4ba6b68a818cb0a0c271e189aeeb1db5d59d7fe87d7d6327bbe7cfa389619016459196497de3ccdea0e7503ba5799e77aa31bbe1310c312ca17b2c5bcc8fa38f266675e8f154c2516ba09278b846696d37213ab9d20a5eb42b03db3173ce490a2ef3b2f3b3600579fc63a0e9041059114f9c910adeca12dbba1fef79b2e2c8899f2d7213cd22dfe4310561a047c59da56bb2bf348c9dd2a2e8f5538a92b904b661cfe54a4298b85868bbe4858080", + "0xf8518080808080a0d546c4ca227a267d29796643032422374624ed109b3d94848c5dc06baceaee76808080808080a027c48e210ccc6e01686be2d4a199d35f0e1e8df624a8d3a17c163be8861acd6680808080", + "0xf843a0207b2b5166478fd4318d2acc6cc2c704584312bdd8781b32d5d06abda57f4230a1a0db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71" + ]) + }, + StorageProof { + key: slot_24, + nibbles: Nibbles::unpack(keccak256(slot_24)), + value: U256::from_str("0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c").unwrap(), + proof: convert_to_proof([ + "0xf9019180a0aafd5b14a6edacd149e110ba6776a654f2dbffca340902be933d011113f2750380a0a502c93b1918c4c6534d4593ae03a5a23fa10ebc30ffb7080b297bff2446e42da02eb2bf45fd443bd1df8b6f9c09726a4c6252a0f7896a131a081e39a7f644b38980a0a9cf7f673a0bce76fd40332afe8601542910b48dea44e93933a3e5e930da5d19a0ddf79db0a36d0c8134ba143bcb541cd4795a9a2bae8aca0ba24b8d8963c2a77da0b973ec0f48f710bf79f63688485755cbe87f9d4c68326bb83c26af620802a80ea0f0855349af6bf84afc8bca2eda31c8ef8c5139be1929eeb3da4ba6b68a818cb0a0c271e189aeeb1db5d59d7fe87d7d6327bbe7cfa389619016459196497de3ccdea0e7503ba5799e77aa31bbe1310c312ca17b2c5bcc8fa38f266675e8f154c2516ba09278b846696d37213ab9d20a5eb42b03db3173ce490a2ef3b2f3b3600579fc63a0e9041059114f9c910adeca12dbba1fef79b2e2c8899f2d7213cd22dfe4310561a047c59da56bb2bf348c9dd2a2e8f5538a92b904b661cfe54a4298b85868bbe4858080", + "0xf85180808080a030263404acfee103d0b1019053ff3240fce433c69b709831673285fa5887ce4c80808080808080a0f8f1fbb1f7b482d9860480feebb83ff54a8b6ec1ead61cc7d2f25d7c01659f9c80808080", + "0xf843a020d332d19b93bcabe3cce7ca0c18a052f57e5fd03b4758a09f30f5ddc4b22ec4a1a0c78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c" + ]) + }, + StorageProof { + key: slot_100, + nibbles: Nibbles::unpack(keccak256(slot_100)), + value: U256::ZERO, + proof: convert_to_proof([ + "0xf9019180a0aafd5b14a6edacd149e110ba6776a654f2dbffca340902be933d011113f2750380a0a502c93b1918c4c6534d4593ae03a5a23fa10ebc30ffb7080b297bff2446e42da02eb2bf45fd443bd1df8b6f9c09726a4c6252a0f7896a131a081e39a7f644b38980a0a9cf7f673a0bce76fd40332afe8601542910b48dea44e93933a3e5e930da5d19a0ddf79db0a36d0c8134ba143bcb541cd4795a9a2bae8aca0ba24b8d8963c2a77da0b973ec0f48f710bf79f63688485755cbe87f9d4c68326bb83c26af620802a80ea0f0855349af6bf84afc8bca2eda31c8ef8c5139be1929eeb3da4ba6b68a818cb0a0c271e189aeeb1db5d59d7fe87d7d6327bbe7cfa389619016459196497de3ccdea0e7503ba5799e77aa31bbe1310c312ca17b2c5bcc8fa38f266675e8f154c2516ba09278b846696d37213ab9d20a5eb42b03db3173ce490a2ef3b2f3b3600579fc63a0e9041059114f9c910adeca12dbba1fef79b2e2c8899f2d7213cd22dfe4310561a047c59da56bb2bf348c9dd2a2e8f5538a92b904b661cfe54a4298b85868bbe4858080", + "0xf891a090bacef44b189ddffdc5f22edc70fe298c58e5e523e6e1dfdf7dbc6d657f7d1b80a026eed68746028bc369eb456b7d3ee475aa16f34e5eaa0c98fdedb9c59ebc53b0808080a09ce86197173e14e0633db84ce8eea32c5454eebe954779255644b45b717e8841808080a0328c7afb2c58ef3f8c4117a8ebd336f1a61d24591067ed9c5aae94796cac987d808080808080" + ]) + }, + ]) + }; + + let account_proof = Proof::new(&tx).account_proof(target, &slots).unwrap(); + pretty_assertions::assert_eq!(account_proof, expected); } } diff --git a/crates/trie/testdata/proof-genesis.json b/crates/trie/testdata/proof-genesis.json new file mode 100644 index 000000000..346aa3e47 --- /dev/null +++ b/crates/trie/testdata/proof-genesis.json @@ -0,0 +1,41 @@ +{ + "config": { + "chainId": 12345, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "shanghaiTime": 0 + }, + "alloc": { + "0x2031f89b3ea8014eb51a78c316e42af3e0d7695f": { + "balance": "0x270801d946c940000" + }, + "0x33f0fc440b8477fcfbe9d0bf8649e7dea9baedb2": { + "balance": "0x1" + }, + "0x62b0dd4aab2b1a0a04e279e2b828791a10755528": { + "balance": "0xf43fc2c04ee0000" + }, + "0x1ed9b1dd266b607ee278726d324b855a093394a6": { + "balance": "0x1aa535d3d0c0000" + } + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x01", + "extraData": "", + "gasLimit": "0x17D7840", + "nonce": "0x1234", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "1695902100" +} \ No newline at end of file