diff --git a/Cargo.lock b/Cargo.lock index 9f24d2979..c23d8b5a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7290,6 +7290,7 @@ version = "1.0.3" dependencies = [ "alloy-eips", "alloy-primitives", + "alloy-rlp", "reth-consensus", "reth-prune-types", "reth-storage-errors", @@ -8647,6 +8648,7 @@ dependencies = [ name = "reth-storage-errors" version = "1.0.3" dependencies = [ + "alloy-rlp", "reth-fs-util", "reth-primitives", "thiserror-no-std", diff --git a/crates/evm/execution-errors/Cargo.toml b/crates/evm/execution-errors/Cargo.toml index 8ec3a7024..b60067dfd 100644 --- a/crates/evm/execution-errors/Cargo.toml +++ b/crates/evm/execution-errors/Cargo.toml @@ -17,6 +17,7 @@ reth-storage-errors.workspace = true reth-prune-types.workspace = true alloy-primitives.workspace = true +alloy-rlp.workspace = true alloy-eips.workspace = true revm-primitives.workspace = true diff --git a/crates/evm/execution-errors/src/lib.rs b/crates/evm/execution-errors/src/lib.rs index 1fdee9856..5d8ec12bd 100644 --- a/crates/evm/execution-errors/src/lib.rs +++ b/crates/evm/execution-errors/src/lib.rs @@ -23,7 +23,7 @@ use revm_primitives::EVMError; use alloc::{boxed::Box, string::String}; pub mod trie; -pub use trie::{StateRootError, StorageRootError}; +pub use trie::*; /// Transaction validation errors #[derive(thiserror_no_std::Error, Debug, Clone, PartialEq, Eq)] diff --git a/crates/evm/execution-errors/src/trie.rs b/crates/evm/execution-errors/src/trie.rs index fd3533977..5690bc97e 100644 --- a/crates/evm/execution-errors/src/trie.rs +++ b/crates/evm/execution-errors/src/trie.rs @@ -1,14 +1,34 @@ //! Errors when computing the state root. -use reth_storage_errors::db::DatabaseError; +use reth_storage_errors::{db::DatabaseError, provider::ProviderError}; use thiserror_no_std::Error; +/// State root errors. +#[derive(Error, Debug, PartialEq, Eq, Clone)] +pub enum StateProofError { + /// Internal database error. + #[error(transparent)] + Database(#[from] DatabaseError), + /// RLP decoding error. + #[error(transparent)] + Rlp(#[from] alloy_rlp::Error), +} + +impl From for ProviderError { + fn from(value: StateProofError) -> Self { + match value { + StateProofError::Database(error) => Self::Database(error), + StateProofError::Rlp(error) => Self::Rlp(error), + } + } +} + /// State root errors. #[derive(Error, Debug, PartialEq, Eq, Clone)] pub enum StateRootError { /// Internal database error. #[error(transparent)] - DB(#[from] DatabaseError), + Database(#[from] DatabaseError), /// Storage root error. #[error(transparent)] StorageRootError(#[from] StorageRootError), @@ -17,8 +37,8 @@ pub enum StateRootError { impl From for DatabaseError { fn from(err: StateRootError) -> Self { match err { - StateRootError::DB(err) | - StateRootError::StorageRootError(StorageRootError::DB(err)) => err, + StateRootError::Database(err) | + StateRootError::StorageRootError(StorageRootError::Database(err)) => err, } } } @@ -28,5 +48,5 @@ impl From for DatabaseError { pub enum StorageRootError { /// Internal database error. #[error(transparent)] - DB(#[from] DatabaseError), + Database(#[from] DatabaseError), } diff --git a/crates/storage/errors/Cargo.toml b/crates/storage/errors/Cargo.toml index d8e699f8d..5ef6f1577 100644 --- a/crates/storage/errors/Cargo.toml +++ b/crates/storage/errors/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true workspace = true [dependencies] +alloy-rlp.workspace = true reth-primitives.workspace = true reth-fs-util.workspace = true diff --git a/crates/storage/errors/src/provider.rs b/crates/storage/errors/src/provider.rs index c3d47aa0b..0979156ca 100644 --- a/crates/storage/errors/src/provider.rs +++ b/crates/storage/errors/src/provider.rs @@ -21,6 +21,9 @@ pub enum ProviderError { /// Database error. #[error(transparent)] Database(#[from] crate::db::DatabaseError), + /// RLP error. + #[error(transparent)] + Rlp(#[from] alloy_rlp::Error), /// Filesystem path error. #[error("{0}")] FsPathError(String), diff --git a/crates/storage/provider/src/providers/state/historical.rs b/crates/storage/provider/src/providers/state/historical.rs index 89767fef6..cbef08dce 100644 --- a/crates/storage/provider/src/providers/state/historical.rs +++ b/crates/storage/provider/src/providers/state/historical.rs @@ -286,7 +286,7 @@ impl<'b, TX: DbTx> StateProofProvider for HistoricalStateProviderRef<'b, TX> { let mut revert_state = self.revert_state()?; revert_state.extend(hashed_state.clone()); Proof::overlay_account_proof(self.tx, revert_state, address, slots) - .map_err(|err| ProviderError::Database(err.into())) + .map_err(Into::::into) } } diff --git a/crates/storage/provider/src/providers/state/latest.rs b/crates/storage/provider/src/providers/state/latest.rs index 723f9b5db..8c95c8c26 100644 --- a/crates/storage/provider/src/providers/state/latest.rs +++ b/crates/storage/provider/src/providers/state/latest.rs @@ -96,8 +96,8 @@ impl<'b, TX: DbTx> StateProofProvider for LatestStateProviderRef<'b, TX> { address: Address, slots: &[B256], ) -> ProviderResult { - Ok(Proof::overlay_account_proof(self.tx, hashed_state.clone(), address, slots) - .map_err(Into::::into)?) + Proof::overlay_account_proof(self.tx, hashed_state.clone(), address, slots) + .map_err(Into::::into) } } diff --git a/crates/trie/common/src/lib.rs b/crates/trie/common/src/lib.rs index ee19b7ed9..bdec36028 100644 --- a/crates/trie/common/src/lib.rs +++ b/crates/trie/common/src/lib.rs @@ -26,7 +26,7 @@ pub use subnode::StoredSubNode; mod proofs; #[cfg(any(test, feature = "test-utils"))] pub use proofs::triehash; -pub use proofs::{AccountProof, StorageProof}; +pub use proofs::*; pub mod root; diff --git a/crates/trie/common/src/proofs.rs b/crates/trie/common/src/proofs.rs index 11953a48d..8fa72e239 100644 --- a/crates/trie/common/src/proofs.rs +++ b/crates/trie/common/src/proofs.rs @@ -2,12 +2,121 @@ use crate::{Nibbles, TrieAccount}; use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; -use alloy_rlp::encode_fixed_size; +use alloy_rlp::{encode_fixed_size, Decodable}; use alloy_trie::{ + nodes::TrieNode, proof::{verify_proof, ProofVerificationError}, EMPTY_ROOT_HASH, }; -use reth_primitives_traits::Account; +use reth_primitives_traits::{constants::KECCAK_EMPTY, Account}; +use std::collections::{BTreeMap, HashMap}; + +/// The state multiproof of target accounts and multiproofs of their storage tries. +#[derive(Clone, Default, Debug)] +pub struct MultiProof { + /// State trie multiproof for requested accounts. + pub account_subtree: BTreeMap, + /// Storage trie multiproofs. + pub storage_multiproofs: HashMap, +} + +impl MultiProof { + /// Construct the account proof from the multiproof. + pub fn account_proof( + &self, + address: Address, + slots: &[B256], + ) -> Result { + let hashed_address = keccak256(address); + let nibbles = Nibbles::unpack(hashed_address); + + // Retrieve the account proof. + let proof = self + .account_subtree + .iter() + .filter(|(path, _)| nibbles.starts_with(path)) + .map(|(_, node)| node.clone()) + .collect::>(); + + // Inspect the last node in the proof. If it's a leaf node with matching suffix, + // then the node contains the encoded trie account. + let info = 'info: { + if let Some(last) = proof.last() { + if let TrieNode::Leaf(leaf) = TrieNode::decode(&mut &last[..])? { + if nibbles.ends_with(&leaf.key) { + let account = TrieAccount::decode(&mut &leaf.value[..])?; + break 'info Some(Account { + balance: account.balance, + nonce: account.nonce, + bytecode_hash: (account.code_hash != KECCAK_EMPTY) + .then_some(account.code_hash), + }) + } + } + } + None + }; + + // Retrieve proofs for requested storage slots. + let storage_multiproof = self.storage_multiproofs.get(&hashed_address); + let storage_root = storage_multiproof.map(|m| m.root).unwrap_or(EMPTY_ROOT_HASH); + let mut storage_proofs = Vec::with_capacity(slots.len()); + for slot in slots { + let proof = if let Some(multiproof) = &storage_multiproof { + multiproof.storage_proof(*slot)? + } else { + StorageProof::new(*slot) + }; + storage_proofs.push(proof); + } + Ok(AccountProof { address, info, proof, storage_root, storage_proofs }) + } +} + +/// The merkle multiproof of storage trie. +#[derive(Clone, Debug)] +pub struct StorageMultiProof { + /// Storage trie root. + pub root: B256, + /// Storage multiproof for requested slots. + pub subtree: BTreeMap, +} + +impl Default for StorageMultiProof { + fn default() -> Self { + Self { root: EMPTY_ROOT_HASH, subtree: BTreeMap::default() } + } +} + +impl StorageMultiProof { + /// Return storage proofs for the target storage slot (unhashed). + pub fn storage_proof(&self, slot: B256) -> Result { + let nibbles = Nibbles::unpack(keccak256(slot)); + + // Retrieve the storage proof. + let proof = self + .subtree + .iter() + .filter(|(path, _)| nibbles.starts_with(path)) + .map(|(_, node)| node.clone()) + .collect::>(); + + // Inspect the last node in the proof. If it's a leaf node with matching suffix, + // then the node contains the encoded slot value. + let value = 'value: { + if let Some(last) = proof.last() { + if let TrieNode::Leaf(leaf) = TrieNode::decode(&mut &last[..])? { + if nibbles.ends_with(&leaf.key) { + break 'value U256::decode(&mut &leaf.value[..])? + } + } + } + U256::ZERO + }; + + Ok(StorageProof { key: slot, nibbles, value, proof }) + } +} /// The merkle proof with the relevant account info. #[derive(PartialEq, Eq, Debug)] @@ -37,23 +146,6 @@ impl AccountProof { } } - /// 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; - } - /// Verify the storage proofs and account proof against the provided state root. pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> { // Verify storage proofs. @@ -106,16 +198,6 @@ impl StorageProof { 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; - } - /// Verify the proof against the provided storage root. pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> { let expected = diff --git a/crates/trie/db/src/proof.rs b/crates/trie/db/src/proof.rs index 090b42cff..09f8098fe 100644 --- a/crates/trie/db/src/proof.rs +++ b/crates/trie/db/src/proof.rs @@ -1,5 +1,5 @@ use reth_db_api::transaction::DbTx; -use reth_execution_errors::StateRootError; +use reth_execution_errors::StateProofError; use reth_primitives::{Address, B256}; use reth_trie::{ hashed_cursor::{DatabaseHashedCursorFactory, HashedPostStateCursorFactory}, @@ -19,7 +19,7 @@ pub trait DatabaseProof<'a, TX> { post_state: HashedPostState, address: Address, slots: &[B256], - ) -> Result; + ) -> Result; } impl<'a, TX: DbTx> DatabaseProof<'a, TX> for Proof<&'a TX, DatabaseHashedCursorFactory<'a, TX>> { @@ -33,7 +33,7 @@ impl<'a, TX: DbTx> DatabaseProof<'a, TX> for Proof<&'a TX, DatabaseHashedCursorF post_state: HashedPostState, address: Address, slots: &[B256], - ) -> Result { + ) -> Result { let prefix_sets = post_state.construct_prefix_sets(); let sorted = post_state.into_sorted(); let hashed_cursor_factory = diff --git a/crates/trie/parallel/src/parallel_root.rs b/crates/trie/parallel/src/parallel_root.rs index 87e6ee8a6..b95d38fa4 100644 --- a/crates/trie/parallel/src/parallel_root.rs +++ b/crates/trie/parallel/src/parallel_root.rs @@ -210,7 +210,7 @@ impl From for ProviderError { fn from(error: ParallelStateRootError) -> Self { match error { ParallelStateRootError::Provider(error) => error, - ParallelStateRootError::StorageRoot(StorageRootError::DB(error)) => { + ParallelStateRootError::StorageRoot(StorageRootError::Database(error)) => { Self::Database(error) } } diff --git a/crates/trie/trie/src/proof.rs b/crates/trie/trie/src/proof.rs index 510bc021d..85a254f70 100644 --- a/crates/trie/trie/src/proof.rs +++ b/crates/trie/trie/src/proof.rs @@ -7,9 +7,12 @@ use crate::{ HashBuilder, Nibbles, }; use alloy_rlp::{BufMut, Encodable}; -use reth_execution_errors::{StateRootError, StorageRootError}; -use reth_primitives::{constants::EMPTY_ROOT_HASH, keccak256, Address, B256}; -use reth_trie_common::{proof::ProofRetainer, AccountProof, StorageProof, TrieAccount}; +use reth_execution_errors::trie::StateProofError; +use reth_primitives::{keccak256, Address, B256}; +use reth_trie_common::{ + proof::ProofRetainer, AccountProof, MultiProof, StorageMultiProof, TrieAccount, +}; +use std::collections::HashMap; /// A struct for generating merkle proofs. /// @@ -24,6 +27,8 @@ pub struct Proof { trie_cursor_factory: T, /// A set of prefix sets that have changes. prefix_sets: TriePrefixSetsMut, + /// Proof targets. + targets: HashMap>, } impl Proof { @@ -33,6 +38,7 @@ impl Proof { trie_cursor_factory: t, hashed_cursor_factory: h, prefix_sets: TriePrefixSetsMut::default(), + targets: HashMap::default(), } } @@ -42,6 +48,7 @@ impl Proof { trie_cursor_factory: self.trie_cursor_factory, hashed_cursor_factory, prefix_sets: self.prefix_sets, + targets: self.targets, } } @@ -50,6 +57,12 @@ impl Proof { self.prefix_sets = prefix_sets; self } + + /// Set the target accounts and slots. + pub fn with_targets(mut self, targets: HashMap>) -> Self { + self.targets = targets; + self + } } impl Proof @@ -59,26 +72,34 @@ where { /// Generate an account proof from intermediate nodes. pub fn account_proof( - &self, + 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); + ) -> Result { + Ok(self + .with_targets(HashMap::from([( + keccak256(address), + slots.iter().map(keccak256).collect(), + )])) + .multi_proof()? + .account_proof(address, slots)?) + } + /// Generate a state multiproof according to specified targets. + pub fn multi_proof(&self) -> Result { let hashed_account_cursor = self.hashed_cursor_factory.hashed_account_cursor()?; let trie_cursor = self.trie_cursor_factory.account_trie_cursor()?; // Create the walker. let mut prefix_set = self.prefix_sets.account_prefix_set.clone(); - prefix_set.insert(target_nibbles.clone()); + prefix_set.extend(self.targets.keys().map(Nibbles::unpack)); let walker = TrieWalker::new(trie_cursor, prefix_set.freeze()); // Create a hash builder to rebuild the root node since it is not available in the database. - let retainer = ProofRetainer::from_iter([target_nibbles]); + let retainer = ProofRetainer::from_iter(self.targets.keys().map(Nibbles::unpack)); let mut hash_builder = HashBuilder::default().with_proof_retainer(retainer); + let mut storage_multiproofs = HashMap::default(); let mut account_rlp = Vec::with_capacity(128); let mut account_node_iter = TrieNodeIter::new(walker, hashed_account_cursor); while let Some(account_node) = account_node_iter.try_next()? { @@ -87,55 +108,40 @@ where hash_builder.add_branch(node.key, node.value, node.children_are_in_trie); } TrieElement::Leaf(hashed_address, account) => { - 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)? - }; + let storage_multiproof = self.storage_multiproof(hashed_address)?; + // Encode account account_rlp.clear(); - let account = TrieAccount::from((account, storage_root)); + let account = TrieAccount::from((account, storage_multiproof.root)); account.encode(&mut account_rlp as &mut dyn BufMut); hash_builder.add_leaf(Nibbles::unpack(hashed_address), &account_rlp); + storage_multiproofs.insert(hashed_address, storage_multiproof); } } } - let _ = hash_builder.root(); - - let proofs = hash_builder.take_proofs(); - account_proof.set_proof(proofs.values().cloned().collect()); - - Ok(account_proof) + Ok(MultiProof { account_subtree: hash_builder.take_proofs(), storage_multiproofs }) } - /// 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( + /// Generate a storage multiproof according to specified targets. + pub fn storage_multiproof( &self, hashed_address: B256, - slots: &[B256], - ) -> Result<(B256, Vec), StorageRootError> { + ) -> Result { let mut hashed_storage_cursor = self.hashed_cursor_factory.hashed_storage_cursor(hashed_address)?; - let mut proofs = slots.iter().copied().map(StorageProof::new).collect::>(); - // short circuit on empty storage if hashed_storage_cursor.is_storage_empty()? { - return Ok((EMPTY_ROOT_HASH, proofs)) + return Ok(StorageMultiProof::default()) } - let target_nibbles = proofs.iter().map(|p| p.nibbles.clone()).collect::>(); + let target_nibbles = self + .targets + .get(&hashed_address) + .map_or(Vec::new(), |slots| slots.iter().map(Nibbles::unpack).collect()); + let mut prefix_set = self.prefix_sets.storage_prefix_sets.get(&hashed_address).cloned().unwrap_or_default(); prefix_set.extend(target_nibbles.clone()); @@ -151,28 +157,15 @@ where hash_builder.add_branch(node.key, node.value, node.children_are_in_trie); } TrieElement::Leaf(hashed_slot, value) => { - let nibbles = Nibbles::unpack(hashed_slot); - if let Some(proof) = proofs.iter_mut().find(|proof| proof.nibbles == nibbles) { - proof.set_value(value); - } - hash_builder.add_leaf(nibbles, alloy_rlp::encode_fixed_size(&value).as_ref()); + hash_builder.add_leaf( + Nibbles::unpack(hashed_slot), + alloy_rlp::encode_fixed_size(&value).as_ref(), + ); } } } let root = hash_builder.root(); - - let all_proof_nodes = hash_builder.take_proofs(); - for proof in &mut proofs { - // Iterate over all proof nodes and find the matching ones. - // The filtered results are guaranteed to be in order. - let matching_proof_nodes = all_proof_nodes - .iter() - .filter(|(path, _)| proof.nibbles.starts_with(path)) - .map(|(_, node)| node.clone()); - proof.set_proof(matching_proof_nodes.collect()); - } - - Ok((root, proofs)) + Ok(StorageMultiProof { root, subtree: hash_builder.take_proofs() }) } }