Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(trie): witness empty root node #10972

Merged
merged 1 commit into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions crates/trie/common/src/proofs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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<StorageProof, alloy_rlp::Error> {
let nibbles = Nibbles::unpack(keccak256(slot));
Expand Down Expand Up @@ -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<Bytes>) -> Self {
self.proof = proof;
self
}

/// Verify the proof against the provided storage root.
pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> {
let expected =
Expand Down
6 changes: 5 additions & 1 deletion crates/trie/db/tests/proof.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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(()));
Expand Down
57 changes: 57 additions & 0 deletions crates/trie/db/tests/witness.rs
Original file line number Diff line number Diff line change
@@ -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])));
}
2 changes: 1 addition & 1 deletion crates/trie/trie/src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>();
Expand Down
17 changes: 12 additions & 5 deletions crates/trie/trie/src/witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -215,7 +217,8 @@ where
proof: impl IntoIterator<Item = (&'b Nibbles, &'b Bytes)>,
) -> Result<BTreeMap<Nibbles, Either<B256, Vec<u8>>>, 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());

Expand All @@ -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))
}
}
};
}

Expand Down
Loading