use std::iter::Peekable; use crate::{SparseStateTrieError, SparseStateTrieResult, SparseTrie}; use alloy_primitives::{ map::{HashMap, HashSet}, Bytes, B256, }; use alloy_rlp::Decodable; use reth_trie::{ updates::{StorageTrieUpdates, TrieUpdates}, Nibbles, TrieNode, }; /// Sparse state trie representing lazy-loaded Ethereum state trie. #[derive(Default, Debug)] pub struct SparseStateTrie { retain_updates: bool, /// Sparse account trie. state: SparseTrie, /// Sparse storage tries. storages: HashMap, /// Collection of revealed account and storage keys. revealed: HashMap>, /// Collection of addresses that had their storage tries wiped. wiped_storages: HashSet, } impl SparseStateTrie { /// Create state trie from state trie. pub fn from_state(state: SparseTrie) -> Self { Self { state, ..Default::default() } } /// Set the retention of branch node updates and deletions. pub const fn with_updates(mut self, retain_updates: bool) -> Self { self.retain_updates = retain_updates; self } /// Returns `true` if account was already revealed. pub fn is_account_revealed(&self, account: &B256) -> bool { self.revealed.contains_key(account) } /// Returns `true` if storage slot for account was already revealed. pub fn is_storage_slot_revealed(&self, account: &B256, slot: &B256) -> bool { self.revealed.get(account).is_some_and(|slots| slots.contains(slot)) } /// Reveal unknown trie paths from provided leaf path and its proof for the account. /// NOTE: This method does not extensively validate the proof. pub fn reveal_account( &mut self, account: B256, proof: impl IntoIterator, ) -> SparseStateTrieResult<()> { if self.is_account_revealed(&account) { return Ok(()); } let mut proof = proof.into_iter().peekable(); let Some(root_node) = self.validate_proof(&mut proof)? else { return Ok(()) }; // Reveal root node if it wasn't already. let trie = self.state.reveal_root(root_node, self.retain_updates)?; // Reveal the remaining proof nodes. for (path, bytes) in proof { let node = TrieNode::decode(&mut &bytes[..])?; trie.reveal_node(path, node)?; } // Mark leaf path as revealed. self.revealed.entry(account).or_default(); Ok(()) } /// Reveal unknown trie paths from provided leaf path and its proof for the storage slot. /// NOTE: This method does not extensively validate the proof. pub fn reveal_storage_slot( &mut self, account: B256, slot: B256, proof: impl IntoIterator, ) -> SparseStateTrieResult<()> { if self.is_storage_slot_revealed(&account, &slot) { return Ok(()); } let mut proof = proof.into_iter().peekable(); let Some(root_node) = self.validate_proof(&mut proof)? else { return Ok(()) }; // Reveal root node if it wasn't already. let trie = self .storages .entry(account) .or_default() .reveal_root(root_node, self.retain_updates)?; // Reveal the remaining proof nodes. for (path, bytes) in proof { let node = TrieNode::decode(&mut &bytes[..])?; trie.reveal_node(path, node)?; } // Mark leaf path as revealed. self.revealed.entry(account).or_default().insert(slot); Ok(()) } /// Validates the root node of the proof and returns it if it exists and is valid. fn validate_proof>( &self, proof: &mut Peekable, ) -> SparseStateTrieResult> { let mut proof = proof.into_iter().peekable(); // Validate root node. let Some((path, node)) = proof.next() else { return Ok(None) }; if !path.is_empty() { return Err(SparseStateTrieError::InvalidRootNode { path, node }) } // Decode root node and perform sanity check. let root_node = TrieNode::decode(&mut &node[..])?; if matches!(root_node, TrieNode::EmptyRoot) && proof.peek().is_some() { return Err(SparseStateTrieError::InvalidRootNode { path, node }) } Ok(Some(root_node)) } /// Update the account leaf node. pub fn update_account_leaf( &mut self, path: Nibbles, value: Vec, ) -> SparseStateTrieResult<()> { self.state.update_leaf(path, value)?; Ok(()) } /// Remove the account leaf node. pub fn remove_account_leaf(&mut self, path: &Nibbles) -> SparseStateTrieResult<()> { self.state.remove_leaf(path)?; Ok(()) } /// Returns sparse trie root if the trie has been revealed. pub fn root(&mut self) -> Option { self.state.root() } /// Calculates the hashes of the nodes below the provided level. pub fn calculate_below_level(&mut self, level: usize) { self.state.calculate_below_level(level); } /// Update the leaf node of a storage trie at the provided address. pub fn update_storage_leaf( &mut self, address: B256, slot: Nibbles, value: Vec, ) -> SparseStateTrieResult<()> { self.storages.entry(address).or_default().update_leaf(slot, value)?; Ok(()) } /// Wipe the storage trie at the provided address. pub fn wipe_storage(&mut self, address: B256) -> SparseStateTrieResult<()> { let Some(trie) = self.storages.get_mut(&address) else { return Ok(()) }; self.wiped_storages.insert(address); trie.wipe().map_err(Into::into) } /// Returns storage sparse trie root if the trie has been revealed. pub fn storage_root(&mut self, account: B256) -> Option { self.storages.get_mut(&account).and_then(|trie| trie.root()) } /// Returns [`TrieUpdates`] by taking the updates from the revealed sparse tries. /// /// Returns `None` if the accounts trie is not revealed. pub fn take_trie_updates(&mut self) -> Option { self.state.as_revealed_mut().map(|state| { let updates = state.take_updates(); TrieUpdates { account_nodes: HashMap::from_iter(updates.updated_nodes), removed_nodes: HashSet::from_iter(updates.removed_nodes), storage_tries: self .storages .iter_mut() .map(|(address, trie)| { let trie = trie.as_revealed_mut().unwrap(); let updates = trie.take_updates(); let updates = StorageTrieUpdates { is_deleted: self.wiped_storages.contains(address), storage_nodes: HashMap::from_iter(updates.updated_nodes), removed_nodes: HashSet::from_iter(updates.removed_nodes), }; (*address, updates) }) .filter(|(_, updates)| !updates.is_empty()) .collect(), } }) } } #[cfg(test)] mod tests { use super::*; use alloy_primitives::{b256, Bytes, U256}; use alloy_rlp::EMPTY_STRING_CODE; use arbitrary::Arbitrary; use assert_matches::assert_matches; use rand::{rngs::StdRng, Rng, SeedableRng}; use reth_primitives_traits::Account; use reth_trie::{ updates::StorageTrieUpdates, BranchNodeCompact, HashBuilder, TrieAccount, TrieMask, EMPTY_ROOT_HASH, }; use reth_trie_common::proof::ProofRetainer; #[test] fn validate_proof_first_node_not_root() { let sparse = SparseStateTrie::default(); let proof = [(Nibbles::from_nibbles([0x1]), Bytes::from([EMPTY_STRING_CODE]))]; assert_matches!( sparse.validate_proof(&mut proof.into_iter().peekable()), Err(SparseStateTrieError::InvalidRootNode { .. }) ); } #[test] fn validate_proof_invalid_proof_with_empty_root() { let sparse = SparseStateTrie::default(); let proof = [ (Nibbles::default(), Bytes::from([EMPTY_STRING_CODE])), (Nibbles::from_nibbles([0x1]), Bytes::new()), ]; assert_matches!( sparse.validate_proof(&mut proof.into_iter().peekable()), Err(SparseStateTrieError::InvalidRootNode { .. }) ); } #[test] fn reveal_account_empty() { let retainer = ProofRetainer::from_iter([Nibbles::default()]); let mut hash_builder = HashBuilder::default().with_proof_retainer(retainer); hash_builder.root(); let proofs = hash_builder.take_proof_nodes(); assert_eq!(proofs.len(), 1); let mut sparse = SparseStateTrie::default(); assert_eq!(sparse.state, SparseTrie::Blind); sparse.reveal_account(Default::default(), proofs.into_inner()).unwrap(); assert_eq!(sparse.state, SparseTrie::revealed_empty()); } #[test] fn reveal_storage_slot_empty() { let retainer = ProofRetainer::from_iter([Nibbles::default()]); let mut hash_builder = HashBuilder::default().with_proof_retainer(retainer); hash_builder.root(); let proofs = hash_builder.take_proof_nodes(); assert_eq!(proofs.len(), 1); let mut sparse = SparseStateTrie::default(); assert!(sparse.storages.is_empty()); sparse .reveal_storage_slot(Default::default(), Default::default(), proofs.into_inner()) .unwrap(); assert_eq!( sparse.storages, HashMap::from_iter([(Default::default(), SparseTrie::revealed_empty())]) ); } #[test] fn take_trie_updates() { reth_tracing::init_test_tracing(); // let mut rng = generators::rng(); let mut rng = StdRng::seed_from_u64(1); let mut bytes = [0u8; 1024]; rng.fill(bytes.as_mut_slice()); let slot_1 = b256!("1000000000000000000000000000000000000000000000000000000000000000"); let slot_path_1 = Nibbles::unpack(slot_1); let value_1 = U256::from(rng.gen::()); let slot_2 = b256!("1100000000000000000000000000000000000000000000000000000000000000"); let slot_path_2 = Nibbles::unpack(slot_2); let value_2 = U256::from(rng.gen::()); let slot_3 = b256!("2000000000000000000000000000000000000000000000000000000000000000"); let slot_path_3 = Nibbles::unpack(slot_3); let value_3 = U256::from(rng.gen::()); let mut storage_hash_builder = HashBuilder::default().with_proof_retainer(ProofRetainer::from_iter([ slot_path_1.clone(), slot_path_2.clone(), ])); storage_hash_builder.add_leaf(slot_path_1.clone(), &alloy_rlp::encode_fixed_size(&value_1)); storage_hash_builder.add_leaf(slot_path_2.clone(), &alloy_rlp::encode_fixed_size(&value_2)); let storage_root = storage_hash_builder.root(); let proof_nodes = storage_hash_builder.take_proof_nodes(); let storage_proof_1 = proof_nodes.matching_nodes_sorted(&slot_path_1); let storage_proof_2 = proof_nodes.matching_nodes_sorted(&slot_path_2); let address_1 = b256!("1000000000000000000000000000000000000000000000000000000000000000"); let address_path_1 = Nibbles::unpack(address_1); let account_1 = Account::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); let mut trie_account_1 = TrieAccount::from((account_1, storage_root)); let address_2 = b256!("1100000000000000000000000000000000000000000000000000000000000000"); let address_path_2 = Nibbles::unpack(address_2); let account_2 = Account::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(); let mut trie_account_2 = TrieAccount::from((account_2, EMPTY_ROOT_HASH)); let mut hash_builder = HashBuilder::default().with_proof_retainer(ProofRetainer::from_iter([ address_path_1.clone(), address_path_2.clone(), ])); hash_builder.add_leaf(address_path_1.clone(), &alloy_rlp::encode(trie_account_1)); hash_builder.add_leaf(address_path_2.clone(), &alloy_rlp::encode(trie_account_2)); let root = hash_builder.root(); let proof_nodes = hash_builder.take_proof_nodes(); let proof_1 = proof_nodes.matching_nodes_sorted(&address_path_1); let proof_2 = proof_nodes.matching_nodes_sorted(&address_path_2); let mut sparse = SparseStateTrie::default().with_updates(true); sparse.reveal_account(address_1, proof_1).unwrap(); sparse.reveal_account(address_2, proof_2).unwrap(); sparse.reveal_storage_slot(address_1, slot_1, storage_proof_1.clone()).unwrap(); sparse.reveal_storage_slot(address_1, slot_2, storage_proof_2.clone()).unwrap(); sparse.reveal_storage_slot(address_2, slot_1, storage_proof_1).unwrap(); sparse.reveal_storage_slot(address_2, slot_2, storage_proof_2).unwrap(); assert_eq!(sparse.root(), Some(root)); let address_3 = b256!("2000000000000000000000000000000000000000000000000000000000000000"); let address_path_3 = Nibbles::unpack(address_3); let account_3 = Account { nonce: account_1.nonce + 1, ..account_1 }; let trie_account_3 = TrieAccount::from((account_3, EMPTY_ROOT_HASH)); sparse.update_account_leaf(address_path_3, alloy_rlp::encode(trie_account_3)).unwrap(); sparse.update_storage_leaf(address_1, slot_path_3, alloy_rlp::encode(value_3)).unwrap(); trie_account_1.storage_root = sparse.storage_root(address_1).unwrap(); sparse.update_account_leaf(address_path_1, alloy_rlp::encode(trie_account_1)).unwrap(); sparse.wipe_storage(address_2).unwrap(); trie_account_2.storage_root = sparse.storage_root(address_2).unwrap(); sparse.update_account_leaf(address_path_2, alloy_rlp::encode(trie_account_2)).unwrap(); sparse.root(); let sparse_updates = sparse.take_trie_updates().unwrap(); // TODO(alexey): assert against real state root calculation updates pretty_assertions::assert_eq!( sparse_updates, TrieUpdates { account_nodes: HashMap::from_iter([ ( Nibbles::default(), BranchNodeCompact { state_mask: TrieMask::new(0b110), tree_mask: TrieMask::new(0b000), hash_mask: TrieMask::new(0b010), hashes: vec![b256!( "4c4ffbda3569fcf2c24ea2000b4cec86ef8b92cbf9ff415db43184c0f75a212e" )], root_hash: Some(b256!( "60944bd29458529c3065d19f63c6e3d5269596fd3b04ca2e7b318912dc89ca4c" )) }, ), ]), storage_tries: HashMap::from_iter([ ( b256!("1000000000000000000000000000000000000000000000000000000000000000"), StorageTrieUpdates { is_deleted: false, storage_nodes: HashMap::from_iter([( Nibbles::default(), BranchNodeCompact { state_mask: TrieMask::new(0b110), tree_mask: TrieMask::new(0b000), hash_mask: TrieMask::new(0b010), hashes: vec![b256!("5bc8b4fdf51839c1e18b8d6a4bd3e2e52c9f641860f0e4d197b68c2679b0e436")], root_hash: Some(b256!("c44abf1a9e1a92736ac479b20328e8d7998aa8838b6ef52620324c9ce85e3201")) } )]), removed_nodes: HashSet::default() } ), ( b256!("1100000000000000000000000000000000000000000000000000000000000000"), StorageTrieUpdates { is_deleted: true, storage_nodes: HashMap::default(), removed_nodes: HashSet::default() } ) ]), removed_nodes: HashSet::default() } ); } }