diff --git a/crates/primitives/src/chain/spec.rs b/crates/primitives/src/chain/spec.rs index 7430ce92a..9da9720d2 100644 --- a/crates/primitives/src/chain/spec.rs +++ b/crates/primitives/src/chain/spec.rs @@ -3,7 +3,7 @@ use crate::{ EIP1559_DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, EIP1559_DEFAULT_ELASTICITY_MULTIPLIER, EIP1559_INITIAL_BASE_FEE, EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, EMPTY_WITHDRAWALS, }, - proofs::genesis_state_root, + proofs::state_root_ref_unhashed, revm_primitives::{address, b256}, Address, BlockNumber, Chain, ForkFilter, ForkFilterKey, ForkHash, ForkId, Genesis, Hardfork, Head, Header, SealedHeader, B256, EMPTY_OMMER_ROOT_HASH, U256, @@ -606,7 +606,7 @@ impl ChainSpec { difficulty: self.genesis.difficulty, nonce: self.genesis.nonce, extra_data: self.genesis.extra_data.clone(), - state_root: genesis_state_root(&self.genesis.alloc), + state_root: state_root_ref_unhashed(&self.genesis.alloc), timestamp: self.genesis.timestamp, mix_hash: self.genesis.mix_hash, beneficiary: self.genesis.coinbase, @@ -1517,8 +1517,8 @@ impl DepositContract { mod tests { use super::*; use crate::{ - b256, hex, ChainConfig, GenesisAccount, NamedChain, B256, DEV, GOERLI, HOLESKY, MAINNET, - SEPOLIA, U256, + b256, hex, trie::TrieAccount, ChainConfig, GenesisAccount, NamedChain, B256, DEV, GOERLI, + HOLESKY, MAINNET, SEPOLIA, U256, }; use alloy_rlp::Encodable; use bytes::BytesMut; @@ -2520,7 +2520,7 @@ Post-merge hard forks (timestamp based): for (key, expected_rlp) in key_rlp { let account = chainspec.genesis.alloc.get(&key).expect("account should exist"); let mut account_rlp = BytesMut::new(); - account.encode(&mut account_rlp); + TrieAccount::from(account.clone()).encode(&mut account_rlp); assert_eq!(account_rlp, expected_rlp) } diff --git a/crates/primitives/src/genesis.rs b/crates/primitives/src/genesis.rs index 02b4700fe..e1a1ecd53 100644 --- a/crates/primitives/src/genesis.rs +++ b/crates/primitives/src/genesis.rs @@ -1,16 +1,12 @@ use crate::{ - constants::EMPTY_ROOT_HASH, keccak256, serde_helper::{ json_u256::{deserialize_json_ttd_opt, deserialize_json_u256}, num::{u64_hex_or_decimal, u64_hex_or_decimal_opt}, storage::deserialize_storage_map, }, - trie::{HashBuilder, Nibbles}, - Account, Address, Bytes, B256, KECCAK_EMPTY, U256, + Account, Address, Bytes, B256, U256, }; -use alloy_rlp::{encode_fixed_size, length_of_length, Encodable, Header as RlpHeader}; -use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -153,19 +149,6 @@ pub struct GenesisAccount { } impl GenesisAccount { - /// Determines the RLP payload length, without the RLP header. - fn payload_len(&self) -> usize { - let mut len = 0; - len += self.nonce.unwrap_or_default().length(); - len += self.balance.length(); - // rather than rlp-encoding the storage, we just return the length of a single hash - // hashes are a fixed size, so it is safe to use the empty root for this - len += EMPTY_ROOT_HASH.length(); - // we are encoding a hash, so let's just use the length of the empty hash for the code hash - len += KECCAK_EMPTY.length(); - len - } - /// Set the nonce. pub fn with_nonce(mut self, nonce: Option) -> Self { self.nonce = nonce; @@ -191,45 +174,6 @@ impl GenesisAccount { } } -impl Encodable for GenesisAccount { - fn encode(&self, out: &mut dyn bytes::BufMut) { - let header = RlpHeader { list: true, payload_length: self.payload_len() }; - header.encode(out); - - self.nonce.unwrap_or_default().encode(out); - self.balance.encode(out); - self.storage - .as_ref() - .map_or(EMPTY_ROOT_HASH, |storage| { - if storage.is_empty() { - return EMPTY_ROOT_HASH - } - - let storage_with_sorted_hashed_keys = storage - .iter() - .filter(|(_k, &v)| v != B256::ZERO) - .map(|(slot, value)| (keccak256(slot), value)) - .sorted_by_key(|(key, _)| *key); - - let mut hb = HashBuilder::default(); - for (hashed_slot, value) in storage_with_sorted_hashed_keys { - let encoded_value = encode_fixed_size(&U256::from_be_bytes(**value)); - hb.add_leaf(Nibbles::unpack(hashed_slot), &encoded_value); - } - - hb.root() - }) - .encode(out); - self.code.as_ref().map_or(KECCAK_EMPTY, keccak256).encode(out); - } - - fn length(&self) -> usize { - let len = self.payload_len(); - // RLP header length + payload length - len + length_of_length(len) - } -} - impl From for Account { fn from(value: GenesisAccount) -> Self { Account { diff --git a/crates/primitives/src/proofs.rs b/crates/primitives/src/proofs.rs index f4851744f..e2c74b3fb 100644 --- a/crates/primitives/src/proofs.rs +++ b/crates/primitives/src/proofs.rs @@ -3,14 +3,14 @@ use crate::{ constants::EMPTY_OMMER_ROOT_HASH, keccak256, - trie::{HashBuilder, Nibbles}, - Address, GenesisAccount, Header, Receipt, ReceiptWithBloom, ReceiptWithBloomRef, - TransactionSigned, Withdrawal, B256, + trie::{HashBuilder, Nibbles, TrieAccount}, + Address, Header, Receipt, ReceiptWithBloom, ReceiptWithBloomRef, TransactionSigned, Withdrawal, + B256, }; +use alloy_primitives::U256; use alloy_rlp::Encodable; use bytes::{BufMut, BytesMut}; use itertools::Itertools; -use std::collections::HashMap; /// Adjust the index of an item for rlp encoding. pub const fn adjust_index_for_rlp(i: usize, len: usize) -> usize { @@ -164,22 +164,73 @@ pub fn calculate_ommers_root(ommers: &[Header]) -> B256 { keccak256(ommers_rlp) } -/// Calculates the root hash for the state, this corresponds to [geth's -/// `deriveHash`](https://github.com/ethereum/go-ethereum/blob/6c149fd4ad063f7c24d726a73bc0546badd1bc73/core/genesis.go#L119). -pub fn genesis_state_root(genesis_alloc: &HashMap) -> B256 { - let accounts_with_sorted_hashed_keys = genesis_alloc - .iter() - .map(|(address, account)| (keccak256(address), account)) - .sorted_by_key(|(key, _)| *key); +/// Hashes and sorts account keys, then proceeds to calculating the root hash of the state +/// represented as MPT. +/// See [state_root_unsorted] for more info. +pub fn state_root_ref_unhashed<'a, A: Into + Clone + 'a>( + state: impl IntoIterator, +) -> B256 { + state_root_unsorted( + state.into_iter().map(|(address, account)| (keccak256(address), account.clone())), + ) +} +/// Hashes and sorts account keys, then proceeds to calculating the root hash of the state +/// represented as MPT. +/// See [state_root_unsorted] for more info. +pub fn state_root_unhashed>( + state: impl IntoIterator, +) -> B256 { + state_root_unsorted(state.into_iter().map(|(address, account)| (keccak256(address), account))) +} + +/// Sorts the hashed account keys and calculates the root hash of the state represented as MPT. +/// See [state_root] for more info. +pub fn state_root_unsorted>( + state: impl IntoIterator, +) -> B256 { + state_root(state.into_iter().sorted_by_key(|(key, _)| *key)) +} + +/// Calculates the root hash of the state represented as MPT. +/// Corresponds to [geth's `deriveHash`](https://github.com/ethereum/go-ethereum/blob/6c149fd4ad063f7c24d726a73bc0546badd1bc73/core/genesis.go#L119). +/// +/// # Panics +/// +/// If the items are not in sorted order. +pub fn state_root>(state: impl IntoIterator) -> B256 { let mut hb = HashBuilder::default(); let mut account_rlp_buf = Vec::new(); - for (hashed_key, account) in accounts_with_sorted_hashed_keys { + for (hashed_key, account) in state { account_rlp_buf.clear(); - account.encode(&mut account_rlp_buf); + account.into().encode(&mut account_rlp_buf); hb.add_leaf(Nibbles::unpack(hashed_key), &account_rlp_buf); } + hb.root() +} +/// Hashes storage keys, sorts them and them calculates the root hash of the storage trie. +/// See [storage_root_unsorted] for more info. +pub fn storage_root_unhashed(storage: impl IntoIterator) -> B256 { + storage_root_unsorted(storage.into_iter().map(|(slot, value)| (keccak256(slot), value))) +} + +/// Sorts and calculates the root hash of account storage trie. +/// See [storage_root] for more info. +pub fn storage_root_unsorted(storage: impl IntoIterator) -> B256 { + storage_root(storage.into_iter().sorted_by_key(|(key, _)| *key)) +} + +/// Calculates the root hash of account storage trie. +/// +/// # Panics +/// +/// If the items are not in sorted order. +pub fn storage_root(storage: impl IntoIterator) -> B256 { + let mut hb = HashBuilder::default(); + for (hashed_slot, value) in storage { + hb.add_leaf(Nibbles::unpack(hashed_slot), alloy_rlp::encode_fixed_size(&value).as_ref()); + } hb.root() } @@ -216,12 +267,13 @@ mod tests { bloom, constants::EMPTY_ROOT_HASH, hex_literal::hex, - proofs::{calculate_receipt_root, calculate_transaction_root, genesis_state_root}, + proofs::{calculate_receipt_root, calculate_transaction_root}, Address, Block, GenesisAccount, Log, Receipt, ReceiptWithBloom, TxType, B256, GOERLI, HOLESKY, MAINNET, SEPOLIA, U256, }; use alloy_primitives::b256; use alloy_rlp::Decodable; + use std::collections::HashMap; #[test] fn check_transaction_root() { @@ -549,8 +601,8 @@ mod tests { #[test] fn check_empty_state_root() { - let genesis_alloc = HashMap::new(); - let root = genesis_state_root(&genesis_alloc); + let genesis_alloc = HashMap::::new(); + let root = state_root_unhashed(genesis_alloc); assert_eq!(root, EMPTY_ROOT_HASH); } @@ -577,7 +629,7 @@ mod tests { test_addr, GenesisAccount { nonce: None, balance: U256::MAX, code: None, storage: None }, ); - let root = genesis_state_root(&genesis_alloc); + let root = state_root_unhashed(genesis_alloc); assert_eq!(root, expected_root); } @@ -587,7 +639,7 @@ mod tests { fn test_chain_state_roots() { let expected_mainnet_state_root = b256!("d7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544"); - let calculated_mainnet_state_root = genesis_state_root(&MAINNET.genesis.alloc); + let calculated_mainnet_state_root = state_root_ref_unhashed(&MAINNET.genesis.alloc); assert_eq!( expected_mainnet_state_root, calculated_mainnet_state_root, "mainnet state root mismatch" @@ -595,7 +647,7 @@ mod tests { let expected_goerli_state_root = b256!("5d6cded585e73c4e322c30c2f782a336316f17dd85a4863b9d838d2d4b8b3008"); - let calculated_goerli_state_root = genesis_state_root(&GOERLI.genesis.alloc); + let calculated_goerli_state_root = state_root_ref_unhashed(&GOERLI.genesis.alloc); assert_eq!( expected_goerli_state_root, calculated_goerli_state_root, "goerli state root mismatch" @@ -603,7 +655,7 @@ mod tests { let expected_sepolia_state_root = b256!("5eb6e371a698b8d68f665192350ffcecbbbf322916f4b51bd79bb6887da3f494"); - let calculated_sepolia_state_root = genesis_state_root(&SEPOLIA.genesis.alloc); + let calculated_sepolia_state_root = state_root_ref_unhashed(&SEPOLIA.genesis.alloc); assert_eq!( expected_sepolia_state_root, calculated_sepolia_state_root, "sepolia state root mismatch" @@ -611,7 +663,7 @@ mod tests { let expected_holesky_state_root = b256!("69d8c9d72f6fa4ad42d4702b433707212f90db395eb54dc20bc85de253788783"); - let calculated_holesky_state_root = genesis_state_root(&HOLESKY.genesis.alloc); + let calculated_holesky_state_root = state_root_ref_unhashed(&HOLESKY.genesis.alloc); assert_eq!( expected_holesky_state_root, calculated_holesky_state_root, "holesky state root mismatch" diff --git a/crates/primitives/src/trie/account.rs b/crates/primitives/src/trie/account.rs new file mode 100644 index 000000000..41e8e8723 --- /dev/null +++ b/crates/primitives/src/trie/account.rs @@ -0,0 +1,59 @@ +use crate::{ + constants::EMPTY_ROOT_HASH, proofs, Account, GenesisAccount, B256, KECCAK_EMPTY, U256, +}; +use alloy_primitives::keccak256; +use alloy_rlp::{RlpDecodable, RlpEncodable}; + +/// An Ethereum account as represented in the trie. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)] +pub struct TrieAccount { + /// Account nonce. + nonce: u64, + /// Account balance. + balance: U256, + /// Account's storage root. + storage_root: B256, + /// Hash of the account's bytecode. + code_hash: B256, +} + +impl From<(Account, B256)> for TrieAccount { + fn from((account, storage_root): (Account, B256)) -> Self { + Self { + nonce: account.nonce, + balance: account.balance, + storage_root, + code_hash: account.bytecode_hash.unwrap_or(KECCAK_EMPTY), + } + } +} + +impl From for TrieAccount { + fn from(account: GenesisAccount) -> Self { + let storage_root = account + .storage + .map(|storage| { + proofs::storage_root_unhashed( + storage + .into_iter() + .filter(|(_, value)| *value != B256::ZERO) + .map(|(slot, value)| (slot, U256::from_be_bytes(*value))), + ) + }) + .unwrap_or(EMPTY_ROOT_HASH); + + Self { + nonce: account.nonce.unwrap_or_default(), + balance: account.balance, + storage_root, + code_hash: account.code.map_or(KECCAK_EMPTY, keccak256), + } + } +} + +impl TrieAccount { + /// Get account's storage root. + pub fn storage_root(&self) -> B256 { + self.storage_root + } +} diff --git a/crates/primitives/src/trie/mod.rs b/crates/primitives/src/trie/mod.rs index b531d8fda..632ce3f4c 100644 --- a/crates/primitives/src/trie/mod.rs +++ b/crates/primitives/src/trie/mod.rs @@ -12,12 +12,14 @@ pub use hash_builder::HashBuilder; mod proofs; pub use proofs::{AccountProof, StorageProof}; +mod account; mod mask; mod nibbles; mod storage; mod subnode; pub use self::{ + account::TrieAccount, mask::TrieMask, nibbles::{Nibbles, StoredNibblesSubKey}, storage::StorageTrieEntry, diff --git a/crates/trie/src/account.rs b/crates/trie/src/account.rs deleted file mode 100644 index 0feeff8ba..000000000 --- a/crates/trie/src/account.rs +++ /dev/null @@ -1,39 +0,0 @@ -use alloy_rlp::{RlpDecodable, RlpEncodable}; -use reth_primitives::{constants::EMPTY_ROOT_HASH, Account, B256, KECCAK_EMPTY, U256}; - -/// An Ethereum account as represented in the trie. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)] -pub struct EthAccount { - /// Account nonce. - nonce: u64, - /// Account balance. - balance: U256, - /// Account's storage root. - storage_root: B256, - /// Hash of the account's bytecode. - code_hash: B256, -} - -impl From for EthAccount { - fn from(acc: Account) -> Self { - EthAccount { - nonce: acc.nonce, - balance: acc.balance, - storage_root: EMPTY_ROOT_HASH, - code_hash: acc.bytecode_hash.unwrap_or(KECCAK_EMPTY), - } - } -} - -impl EthAccount { - /// Set storage root on account. - pub fn with_storage_root(mut self, storage_root: B256) -> Self { - self.storage_root = storage_root; - self - } - - /// Get account's storage root. - pub fn storage_root(&self) -> B256 { - self.storage_root - } -} diff --git a/crates/trie/src/lib.rs b/crates/trie/src/lib.rs index 31183f542..9e842a7bf 100644 --- a/crates/trie/src/lib.rs +++ b/crates/trie/src/lib.rs @@ -15,9 +15,6 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -/// The Ethereum account as represented in the trie. -pub mod account; - /// The implementation of a container for storing intermediate changes to a trie. /// The container indicates when the trie has been modified. pub mod prefix_set; diff --git a/crates/trie/src/proof.rs b/crates/trie/src/proof.rs index eb7c438c4..874c241e2 100644 --- a/crates/trie/src/proof.rs +++ b/crates/trie/src/proof.rs @@ -1,5 +1,4 @@ use crate::{ - account::EthAccount, hashed_cursor::{HashedCursorFactory, HashedStorageCursor}, node_iter::{AccountNode, AccountNodeIter, StorageNode, StorageNodeIter}, prefix_set::PrefixSetMut, @@ -12,7 +11,7 @@ use reth_db::{tables, transaction::DbTx}; use reth_primitives::{ constants::EMPTY_ROOT_HASH, keccak256, - trie::{AccountProof, HashBuilder, Nibbles, StorageProof}, + trie::{AccountProof, HashBuilder, Nibbles, StorageProof, TrieAccount}, Address, B256, }; @@ -81,7 +80,7 @@ where }; account_rlp.clear(); - let account = EthAccount::from(account).with_storage_root(storage_root); + let account = TrieAccount::from((account, storage_root)); account.encode(&mut account_rlp as &mut dyn BufMut); hash_builder.add_leaf(Nibbles::unpack(hashed_address), &account_rlp); diff --git a/crates/trie/src/test_utils.rs b/crates/trie/src/test_utils.rs index bd16f04cd..2d0eb25e0 100644 --- a/crates/trie/src/test_utils.rs +++ b/crates/trie/src/test_utils.rs @@ -1,6 +1,7 @@ -use crate::account::EthAccount; use alloy_rlp::{encode_fixed_size, Encodable}; -use reth_primitives::{proofs::triehash::KeccakHasher, Account, Address, B256, U256}; +use reth_primitives::{ + proofs::triehash::KeccakHasher, trie::TrieAccount, Account, Address, B256, U256, +}; /// Re-export of [triehash]. pub use triehash; @@ -14,7 +15,7 @@ where let encoded_accounts = accounts.map(|(address, (account, storage))| { let storage_root = storage_root(storage.into_iter()); let mut out = Vec::new(); - EthAccount::from(account).with_storage_root(storage_root).encode(&mut out); + TrieAccount::from((account, storage_root)).encode(&mut out); (address, out) }); @@ -37,7 +38,7 @@ where let encoded_accounts = accounts.map(|(address, (account, storage))| { let storage_root = storage_root_prehashed(storage.into_iter()); let mut out = Vec::new(); - EthAccount::from(account).with_storage_root(storage_root).encode(&mut out); + TrieAccount::from((account, storage_root)).encode(&mut out); (address, out) }); diff --git a/crates/trie/src/trie.rs b/crates/trie/src/trie.rs index d145c6634..ad385a65c 100644 --- a/crates/trie/src/trie.rs +++ b/crates/trie/src/trie.rs @@ -1,5 +1,4 @@ use crate::{ - account::EthAccount, hashed_cursor::{HashedCursorFactory, HashedStorageCursor}, node_iter::{AccountNode, AccountNodeIter, StorageNode, StorageNodeIter}, prefix_set::{PrefixSet, PrefixSetLoader, PrefixSetMut}, @@ -14,7 +13,7 @@ use reth_db::{tables, transaction::DbTx}; use reth_primitives::{ constants::EMPTY_ROOT_HASH, keccak256, - trie::{HashBuilder, Nibbles}, + trie::{HashBuilder, Nibbles, TrieAccount}, Address, BlockNumber, B256, }; use std::{ @@ -287,7 +286,7 @@ where storage_root_calculator.root()? }; - let account = EthAccount::from(account).with_storage_root(storage_root); + let account = TrieAccount::from((account, storage_root)); account_rlp.clear(); account.encode(&mut account_rlp as &mut dyn BufMut); @@ -769,10 +768,7 @@ mod tests { } fn encode_account(account: Account, storage_root: Option) -> Vec { - let mut account = EthAccount::from(account); - if let Some(storage_root) = storage_root { - account = account.with_storage_root(storage_root); - } + let account = TrieAccount::from((account, storage_root.unwrap_or(EMPTY_ROOT_HASH))); let mut account_rlp = Vec::with_capacity(account.length()); account.encode(&mut account_rlp); account_rlp