From b5423099ec87c2eb111418367be970035e62194b Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 30 Sep 2024 09:55:58 +0200 Subject: [PATCH] test(trie): witness empty root node --- crates/trie/common/src/proofs.rs | 23 +++++++++---- crates/trie/db/tests/proof.rs | 6 +++- crates/trie/db/tests/witness.rs | 57 ++++++++++++++++++++++++++++++++ crates/trie/trie/src/proof.rs | 2 +- crates/trie/trie/src/witness.rs | 17 +++++++--- 5 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 crates/trie/db/tests/witness.rs diff --git a/crates/trie/common/src/proofs.rs b/crates/trie/common/src/proofs.rs index b35edd96d560..8aca67f8d1ad 100644 --- a/crates/trie/common/src/proofs.rs +++ b/crates/trie/common/src/proofs.rs @@ -2,7 +2,7 @@ use crate::{Nibbles, TrieAccount}; use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; -use alloy_rlp::{encode_fixed_size, Decodable}; +use alloy_rlp::{encode_fixed_size, Decodable, EMPTY_STRING_CODE}; use alloy_trie::{ nodes::TrieNode, proof::{verify_proof, ProofNodes, ProofVerificationError}, @@ -86,13 +86,18 @@ pub struct StorageMultiProof { pub subtree: ProofNodes, } -impl Default for StorageMultiProof { - fn default() -> Self { - Self { root: EMPTY_ROOT_HASH, subtree: Default::default() } +impl StorageMultiProof { + /// Create new storage multiproof for empty trie. + pub fn empty() -> Self { + Self { + root: EMPTY_ROOT_HASH, + subtree: ProofNodes::from_iter([( + Nibbles::default(), + Bytes::from([EMPTY_STRING_CODE]), + )]), + } } -} -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)); @@ -209,6 +214,12 @@ impl StorageProof { Self { key, nibbles, ..Default::default() } } + /// Set proof nodes on storage proof. + pub fn with_proof(mut self, proof: Vec) -> Self { + self.proof = proof; + self + } + /// Verify the proof against the provided storage root. pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> { let expected = diff --git a/crates/trie/db/tests/proof.rs b/crates/trie/db/tests/proof.rs index 33a19de38037..5ffa6729b49a 100644 --- a/crates/trie/db/tests/proof.rs +++ b/crates/trie/db/tests/proof.rs @@ -1,6 +1,7 @@ #![allow(missing_docs)] use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; +use alloy_rlp::EMPTY_STRING_CODE; use reth_chainspec::{Chain, ChainSpec, HOLESKY, MAINNET}; use reth_primitives::{constants::EMPTY_ROOT_HASH, Account}; use reth_provider::test_utils::{create_test_provider_factory, insert_genesis}; @@ -111,7 +112,10 @@ fn testspec_empty_storage_proof() { assert_eq!(slots.len(), account_proof.storage_proofs.len()); for (idx, slot) in slots.into_iter().enumerate() { let proof = account_proof.storage_proofs.get(idx).unwrap(); - assert_eq!(proof, &StorageProof::new(slot)); + assert_eq!( + proof, + &StorageProof::new(slot).with_proof(vec![Bytes::from([EMPTY_STRING_CODE])]) + ); assert_eq!(proof.verify(account_proof.storage_root), Ok(())); } assert_eq!(account_proof.verify(root), Ok(())); diff --git a/crates/trie/db/tests/witness.rs b/crates/trie/db/tests/witness.rs new file mode 100644 index 000000000000..cc921f657087 --- /dev/null +++ b/crates/trie/db/tests/witness.rs @@ -0,0 +1,57 @@ +#![allow(missing_docs)] + +use alloy_primitives::{ + keccak256, + map::{HashMap, HashSet}, + Address, Bytes, B256, U256, +}; +use alloy_rlp::EMPTY_STRING_CODE; +use reth_primitives::{constants::EMPTY_ROOT_HASH, Account}; +use reth_provider::{test_utils::create_test_provider_factory, HashingWriter}; +use reth_trie::{proof::Proof, witness::TrieWitness, HashedPostState, HashedStorage, StateRoot}; +use reth_trie_db::{DatabaseProof, DatabaseStateRoot, DatabaseTrieWitness}; + +#[test] +fn includes_empty_node_preimage() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + + let address = Address::random(); + let hashed_address = keccak256(address); + let hashed_slot = B256::random(); + + // witness includes empty state trie root node + assert_eq!( + TrieWitness::from_tx(provider.tx_ref()) + .compute(HashedPostState { + accounts: HashMap::from([(hashed_address, Some(Account::default()))]), + storages: HashMap::default(), + }) + .unwrap(), + HashMap::from_iter([(EMPTY_ROOT_HASH, Bytes::from([EMPTY_STRING_CODE]))]) + ); + + // Insert account into database + provider.insert_account_for_hashing([(address, Some(Account::default()))]).unwrap(); + + let state_root = StateRoot::from_tx(provider.tx_ref()).root().unwrap(); + let multiproof = Proof::from_tx(provider.tx_ref()) + .multiproof(HashMap::from_iter([(hashed_address, HashSet::from_iter([hashed_slot]))])) + .unwrap(); + + let witness = TrieWitness::from_tx(provider.tx_ref()) + .compute(HashedPostState { + accounts: HashMap::from([(hashed_address, Some(Account::default()))]), + storages: HashMap::from([( + hashed_address, + HashedStorage::from_iter(false, [(hashed_slot, U256::from(1))]), + )]), + }) + .unwrap(); + assert!(witness.contains_key(&state_root)); + for node in multiproof.account_subtree.values() { + assert_eq!(witness.get(&keccak256(node)), Some(node)); + } + // witness includes empty state trie root node + assert_eq!(witness.get(&EMPTY_ROOT_HASH), Some(&Bytes::from([EMPTY_STRING_CODE]))); +} diff --git a/crates/trie/trie/src/proof.rs b/crates/trie/trie/src/proof.rs index 3e9ca5783814..95d9505218bf 100644 --- a/crates/trie/trie/src/proof.rs +++ b/crates/trie/trie/src/proof.rs @@ -192,7 +192,7 @@ where // short circuit on empty storage if hashed_storage_cursor.is_storage_empty()? { - return Ok(StorageMultiProof::default()) + return Ok(StorageMultiProof::empty()) } let target_nibbles = targets.into_iter().map(Nibbles::unpack).collect::>(); diff --git a/crates/trie/trie/src/witness.rs b/crates/trie/trie/src/witness.rs index 972afc10c342..b0fcfb021ae1 100644 --- a/crates/trie/trie/src/witness.rs +++ b/crates/trie/trie/src/witness.rs @@ -17,7 +17,7 @@ use itertools::{Either, Itertools}; use reth_execution_errors::TrieWitnessError; use reth_primitives::constants::EMPTY_ROOT_HASH; use reth_trie_common::{ - BranchNode, HashBuilder, Nibbles, TrieAccount, TrieNode, CHILD_INDEX_RANGE, + BranchNode, HashBuilder, Nibbles, StorageMultiProof, TrieAccount, TrieNode, CHILD_INDEX_RANGE, }; /// State transition witness for the trie. @@ -110,8 +110,10 @@ where let mut account_rlp = Vec::with_capacity(128); let mut account_trie_nodes = BTreeMap::default(); for (hashed_address, hashed_slots) in proof_targets { - let storage_multiproof = - account_multiproof.storages.remove(&hashed_address).unwrap_or_default(); + let storage_multiproof = account_multiproof + .storages + .remove(&hashed_address) + .unwrap_or_else(StorageMultiProof::empty); // Gather and record account trie nodes. let account = state @@ -215,7 +217,8 @@ where proof: impl IntoIterator, ) -> Result>>, TrieWitnessError> { let mut trie_nodes = BTreeMap::default(); - for (path, encoded) in proof { + let mut proof_iter = proof.into_iter().enumerate().peekable(); + while let Some((idx, (path, encoded))) = proof_iter.next() { // Record the node in witness. self.witness.insert(keccak256(encoded.as_ref()), encoded.clone()); @@ -239,7 +242,11 @@ where trie_nodes.insert(next_path.clone(), Either::Right(leaf.value.clone())); } } - TrieNode::EmptyRoot => return Err(TrieWitnessError::UnexpectedEmptyRoot(next_path)), + TrieNode::EmptyRoot => { + if idx != 0 || proof_iter.peek().is_some() { + return Err(TrieWitnessError::UnexpectedEmptyRoot(next_path)) + } + } }; }