From 54aab533c2052ed350fc8a078812d026e663d101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 13 Mar 2023 21:49:36 -0300 Subject: [PATCH] feat(rpc): add partial `eth_getProof` implementation (#1515) Co-authored-by: lambdaclass-user --- crates/executor/src/executor.rs | 8 + crates/interfaces/src/provider.rs | 3 + crates/primitives/src/account.rs | 9 + crates/rpc/rpc-builder/tests/it/http.rs | 4 +- crates/rpc/rpc/src/eth/api/call.rs | 7 +- crates/rpc/rpc/src/eth/api/mod.rs | 65 ++- crates/rpc/rpc/src/eth/api/server.rs | 19 +- crates/rpc/rpc/src/eth/api/state.rs | 56 ++- crates/stages/Cargo.toml | 10 +- crates/stages/src/stages/merkle.rs | 15 +- crates/storage/provider/Cargo.toml | 4 +- crates/storage/provider/src/lib.rs | 2 +- .../src/providers/state/historical.rs | 11 +- .../provider/src/providers/state/latest.rs | 53 ++- .../provider/src/providers/state/macros.rs | 3 +- .../storage/provider/src/test_utils/mock.rs | 8 + .../storage/provider/src/test_utils/noop.rs | 13 +- crates/storage/provider/src/traits/state.rs | 9 +- crates/storage/provider/src/transaction.rs | 2 +- crates/storage/provider/src/trie/mod.rs | 382 +++++++++++++++--- 20 files changed, 578 insertions(+), 105 deletions(-) diff --git a/crates/executor/src/executor.rs b/crates/executor/src/executor.rs index 01a291b3d9..22cc7c391d 100644 --- a/crates/executor/src/executor.rs +++ b/crates/executor/src/executor.rs @@ -599,6 +599,14 @@ mod tests { fn bytecode_by_hash(&self, code_hash: H256) -> reth_interfaces::Result> { Ok(self.contracts.get(&code_hash).cloned()) } + + fn proof( + &self, + _address: Address, + _keys: &[H256], + ) -> reth_interfaces::Result<(Vec, H256, Vec>)> { + todo!() + } } #[test] diff --git a/crates/interfaces/src/provider.rs b/crates/interfaces/src/provider.rs index ac24be67a6..e86d2dc594 100644 --- a/crates/interfaces/src/provider.rs +++ b/crates/interfaces/src/provider.rs @@ -68,6 +68,9 @@ pub enum ProviderError { /// Reached the end of the transaction sender table. #[error("Got to the end of the transaction sender table")] EndOfTransactionSenderTable, + /// Some error occurred while interacting with the state tree. + #[error("Unknown error occurred while interacting with the state tree.")] + StateTree, /// Thrown when required header related data was not found but was required. #[error("requested data not found")] HeaderNotFound, diff --git a/crates/primitives/src/account.rs b/crates/primitives/src/account.rs index e822bc449e..b91796129c 100644 --- a/crates/primitives/src/account.rs +++ b/crates/primitives/src/account.rs @@ -34,6 +34,15 @@ impl Account { self.nonce == 0 && self.balance == U256::ZERO && is_bytecode_empty } + + /// Returns an account bytecode's hash. + /// In case of no bytecode, returns [`KECCAK_EMPTY`]. + pub fn get_bytecode_hash(&self) -> H256 { + match self.bytecode_hash { + Some(hash) => hash, + None => KECCAK_EMPTY, + } + } } /// Bytecode for an account. diff --git a/crates/rpc/rpc-builder/tests/it/http.rs b/crates/rpc/rpc-builder/tests/it/http.rs index 898beae6d8..f55d4bcbf0 100644 --- a/crates/rpc/rpc-builder/tests/it/http.rs +++ b/crates/rpc/rpc-builder/tests/it/http.rs @@ -73,6 +73,7 @@ where EthApiClient::block_by_number(client, block_number, false).await.unwrap(); EthApiClient::block_transaction_count_by_number(client, block_number).await.unwrap(); EthApiClient::block_transaction_count_by_hash(client, hash).await.unwrap(); + EthApiClient::get_proof(client, address, vec![], None).await.unwrap(); EthApiClient::block_uncles_count_by_hash(client, hash).await.unwrap(); EthApiClient::block_uncles_count_by_number(client, block_number).await.unwrap(); EthApiClient::uncle_by_block_hash_and_index(client, hash, index).await.unwrap(); @@ -123,9 +124,6 @@ where .err() .unwrap() )); - assert!(is_unimplemented( - EthApiClient::get_proof(client, address, vec![], None).await.err().unwrap() - )); } async fn test_basic_debug_calls(client: &C) diff --git a/crates/rpc/rpc/src/eth/api/call.rs b/crates/rpc/rpc/src/eth/api/call.rs index cf92895e3c..6c43da516f 100644 --- a/crates/rpc/rpc/src/eth/api/call.rs +++ b/crates/rpc/rpc/src/eth/api/call.rs @@ -25,6 +25,7 @@ use revm::{ }, Database, }; +use std::ops::Deref; // Gas per transaction not creating a contract. const MIN_TRANSACTION_GAS: u64 = 21_000u64; @@ -66,7 +67,7 @@ where ) -> EthResult<(ResultAndState, Env)> { let (cfg, block_env, at) = self.evm_env_at(at).await?; let state = self.state_at_block_id(at)?.ok_or_else(|| EthApiError::UnknownBlockNumber)?; - self.call_with(cfg, block_env, request, state, state_overrides) + self.call_with(cfg, block_env, request, &*state, state_overrides) } /// Executes the call request using the given environment against the state provider @@ -106,7 +107,7 @@ where ) -> EthResult { let (cfg, block_env, at) = self.evm_env_at(at).await?; let state = self.state_at_block_id(at)?.ok_or_else(|| EthApiError::UnknownBlockNumber)?; - self.estimate_gas_with(cfg, block_env, request, state) + self.estimate_gas_with(cfg, block_env, request, &*state) } /// Estimates the gas usage of the `request` with the state. @@ -295,7 +296,7 @@ where cfg.disable_block_gas_limit = true; let env = build_call_evm_env(cfg, block, request.clone())?; - let mut db = SubState::new(State::new(state)); + let mut db = SubState::new(State::new(state.deref())); let from = request.from.unwrap_or_default(); let to = if let Some(to) = request.to { diff --git a/crates/rpc/rpc/src/eth/api/mod.rs b/crates/rpc/rpc/src/eth/api/mod.rs index 29f321b482..97911adf5c 100644 --- a/crates/rpc/rpc/src/eth/api/mod.rs +++ b/crates/rpc/rpc/src/eth/api/mod.rs @@ -10,8 +10,10 @@ use reth_network_api::NetworkInfo; use reth_primitives::{ Address, BlockId, BlockNumberOrTag, ChainInfo, TransactionSigned, H256, U64, }; -use reth_provider::{BlockProvider, EvmEnvProvider, StateProviderFactory}; -use std::num::NonZeroUsize; +use reth_provider::{ + BlockProvider, EvmEnvProvider, StateProvider as StateProviderTrait, StateProviderFactory, +}; +use std::{num::NonZeroUsize, ops::Deref}; use crate::eth::{cache::EthStateCache, error::EthResult}; use reth_provider::providers::ChainState; @@ -97,6 +99,37 @@ impl EthApi { } } +// Transparent wrapper to enable state access helpers +// returning latest state provider when appropiate +pub(crate) enum StateProvider<'a, H, L> { + History(H), + Latest(L), + _Unreachable(&'a ()), // like a PhantomData for 'a +} + +type HistoryOrLatest<'a, Client> = StateProvider< + 'a, + ::HistorySP<'a>, + ::LatestSP<'a>, +>; + +impl<'a, H, L> Deref for StateProvider<'a, H, L> +where + Self: 'a, + H: StateProviderTrait + 'a, + L: StateProviderTrait + 'a, +{ + type Target = dyn StateProviderTrait + 'a; + + fn deref(&self) -> &Self::Target { + match self { + StateProvider::History(h) => h, + StateProvider::Latest(l) => l, + StateProvider::_Unreachable(()) => unreachable!(), + } + } +} + // === State access helpers === impl EthApi @@ -119,11 +152,11 @@ where pub(crate) fn state_at_block_id_or_latest( &self, block_id: Option, - ) -> Result::HistorySP<'_>>> { + ) -> Result>> { if let Some(block_id) = block_id { self.state_at_block_id(block_id) } else { - self.latest_state() + self.latest_state().map(|v| Some(StateProvider::Latest(v))) } } @@ -131,9 +164,11 @@ where pub(crate) fn state_at_block_id( &self, block_id: BlockId, - ) -> Result::HistorySP<'_>>> { + ) -> Result>> { match block_id { - BlockId::Hash(hash) => self.state_at_hash(hash.into()).map(Some), + BlockId::Hash(hash) => { + self.state_at_hash(hash.into()).map(|s| Some(StateProvider::History(s))) + } BlockId::Number(num) => self.state_at_block_number(num), } } @@ -144,7 +179,7 @@ where pub(crate) fn state_at_block_number( &self, num: BlockNumberOrTag, - ) -> Result::HistorySP<'_>>> { + ) -> Result>> { if let Some(number) = self.convert_block_number(num)? { self.state_at_number(number).map(Some) } else { @@ -161,18 +196,16 @@ where } /// Returns the state at the given block number - pub(crate) fn state_at_number( - &self, - block_number: u64, - ) -> Result<::HistorySP<'_>> { - self.client().history_by_block_number(block_number) + pub(crate) fn state_at_number(&self, block_number: u64) -> Result> { + match self.convert_block_number(BlockNumberOrTag::Latest)? { + Some(num) if num == block_number => self.latest_state().map(StateProvider::Latest), + _ => self.client().history_by_block_number(block_number).map(StateProvider::History), + } } /// Returns the _latest_ state - pub(crate) fn latest_state( - &self, - ) -> Result::HistorySP<'_>>> { - self.state_at_block_number(BlockNumberOrTag::Latest) + pub(crate) fn latest_state(&self) -> Result<::LatestSP<'_>> { + self.client().latest() } } diff --git a/crates/rpc/rpc/src/eth/api/server.rs b/crates/rpc/rpc/src/eth/api/server.rs index 0ff4310e0f..f59f25f5d9 100644 --- a/crates/rpc/rpc/src/eth/api/server.rs +++ b/crates/rpc/rpc/src/eth/api/server.rs @@ -374,17 +374,24 @@ where /// Handler for: `eth_getProof` async fn get_proof( &self, - _address: Address, - _keys: Vec, - _block_number: Option, + address: Address, + keys: Vec, + block_number: Option, ) -> Result { - Err(internal_rpc_err("unimplemented")) + let res = EthApi::get_proof(self, address, keys, block_number); + + Ok(res.map_err(|e| match e { + EthApiError::InvalidBlockRange => { + internal_rpc_err("eth_getProof is unimplemented for historical blocks") + } + _ => e.into(), + })?) } } #[cfg(test)] mod tests { - use crate::eth::cache::EthStateCache; + use crate::{eth::cache::EthStateCache, EthApi}; use jsonrpsee::{ core::{error::Error as RpcError, RpcResult}, types::error::{CallError, INVALID_PARAMS_CODE}, @@ -396,8 +403,6 @@ mod tests { use reth_rpc_api::EthApiServer; use reth_transaction_pool::test_utils::testing_pool; - use crate::EthApi; - #[tokio::test] /// Handler for: `eth_test_fee_history` async fn test_fee_history() { diff --git a/crates/rpc/rpc/src/eth/api/state.rs b/crates/rpc/rpc/src/eth/api/state.rs index fce654d13c..42638bcdb9 100644 --- a/crates/rpc/rpc/src/eth/api/state.rs +++ b/crates/rpc/rpc/src/eth/api/state.rs @@ -1,11 +1,15 @@ //! Contains RPC handler implementations specific to state. use crate::{ - eth::error::{EthApiError, EthResult}, + eth::{ + api::StateProvider, + error::{EthApiError, EthResult}, + }, EthApi, }; -use reth_primitives::{Address, BlockId, Bytes, H256, U256}; -use reth_provider::{BlockProvider, EvmEnvProvider, StateProvider, StateProviderFactory}; +use reth_primitives::{Address, BlockId, Bytes, H256, KECCAK_EMPTY, U256}; +use reth_provider::{BlockProvider, EvmEnvProvider, StateProviderFactory}; +use reth_rpc_types::{EIP1186AccountProofResponse, StorageProof}; impl EthApi where @@ -48,4 +52,50 @@ where let value = state.storage(address, storage_key)?.unwrap_or_default(); Ok(H256(value.to_be_bytes())) } + + pub(crate) fn get_proof( + &self, + address: Address, + keys: Vec, + block_id: Option, + ) -> EthResult { + let state = + self.state_at_block_id_or_latest(block_id)?.ok_or(EthApiError::UnknownBlockNumber)?; + + // TODO: remove when HistoricalStateProviderRef::proof is implemented + if matches!(state, StateProvider::History(_)) { + return Err(EthApiError::InvalidBlockRange) + } + + let (account_proof, storage_hash, stg_proofs) = state.proof(address, &keys)?; + + let storage_proof = keys + .into_iter() + .zip(stg_proofs) + .map(|(key, proof)| { + state.storage(address, key).map(|op| StorageProof { + key: U256::from_be_bytes(*key.as_fixed_bytes()), + value: op.unwrap_or_default(), + proof, + }) + }) + .collect::>()?; + + let mut proof = EIP1186AccountProofResponse { + address, + code_hash: KECCAK_EMPTY, + account_proof, + storage_hash, + storage_proof, + ..Default::default() + }; + + if let Some(account) = state.basic_account(address)? { + proof.balance = account.balance; + proof.nonce = account.nonce.into(); + proof.code_hash = account.get_bytecode_hash(); + } + + Ok(proof) + } } diff --git a/crates/stages/Cargo.toml b/crates/stages/Cargo.toml index 3b1970bd71..507ed4cfdf 100644 --- a/crates/stages/Cargo.toml +++ b/crates/stages/Cargo.toml @@ -10,7 +10,7 @@ description = "Staged syncing primitives used in reth." [package.metadata.cargo-udeps.ignore] normal = [ # Used for diagrams in docs - "aquamarine" + "aquamarine", ] [dependencies] @@ -46,7 +46,7 @@ proptest = { version = "1.0", optional = true } [dev-dependencies] # reth -reth-primitives = { path = "../primitives", features = ["arbitrary"]} +reth-primitives = { path = "../primitives", features = ["arbitrary"] } reth-db = { path = "../storage/db", features = ["test-utils", "mdbx"] } reth-interfaces = { path = "../interfaces", features = ["test-utils"] } reth-downloaders = { path = "../net/downloaders" } @@ -60,7 +60,11 @@ rand = "0.8.5" paste = "1.0" # Stage benchmarks -pprof = { version = "0.11", features = ["flamegraph", "frame-pointer", "criterion"] } +pprof = { version = "0.11", features = [ + "flamegraph", + "frame-pointer", + "criterion", +] } criterion = { version = "0.4.0", features = ["async_futures"] } proptest = { version = "1.0" } arbitrary = { version = "1.1.7", features = ["derive"] } diff --git a/crates/stages/src/stages/merkle.rs b/crates/stages/src/stages/merkle.rs index 20ebab25b5..3078742fd1 100644 --- a/crates/stages/src/stages/merkle.rs +++ b/crates/stages/src/stages/merkle.rs @@ -2,7 +2,7 @@ use crate::{ExecInput, ExecOutput, Stage, StageError, StageId, UnwindInput, Unwi use reth_db::{database::Database, tables, transaction::DbTx}; use reth_interfaces::consensus; use reth_provider::{trie::DBTrieLoader, Transaction}; -use std::fmt::Debug; +use std::{fmt::Debug, ops::DerefMut}; use tracing::*; /// The [`StageId`] of the merkle hashing execution stage. @@ -108,14 +108,14 @@ impl Stage for MerkleStage { } else if to_transition - from_transition > threshold || stage_progress == 0 { debug!(target: "sync::stages::merkle::exec", current = ?stage_progress, target = ?previous_stage_progress, "Rebuilding trie"); // if there are more blocks than threshold it is faster to rebuild the trie - DBTrieLoader::::new(tx) + DBTrieLoader::new(tx.deref_mut()) .calculate_root() .map_err(|e| StageError::Fatal(Box::new(e)))? } else { debug!(target: "sync::stages::merkle::exec", current = ?stage_progress, target = ?previous_stage_progress, "Updating trie"); // Iterate over changeset (similar to Hashing stages) and take new values let current_root = tx.get_header(stage_progress)?.state_root; - DBTrieLoader::::new(tx) + DBTrieLoader::new(tx.deref_mut()) .update_root(current_root, from_transition..to_transition) .map_err(|e| StageError::Fatal(Box::new(e)))? }; @@ -160,7 +160,7 @@ impl Stage for MerkleStage { let from_transition = tx.get_block_transition(input.unwind_to)?; let to_transition = tx.get_block_transition(input.stage_progress)?; - let block_root = DBTrieLoader::::new(tx) + let block_root = DBTrieLoader::new(tx.deref_mut()) .update_root(current_root, from_transition..to_transition) .map_err(|e| StageError::Fatal(Box::new(e)))?; @@ -191,6 +191,7 @@ mod tests { use assert_matches::assert_matches; use reth_db::{ cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO, DbDupCursorRW}, + database::DatabaseGAT, mdbx::{Env, WriteMap}, tables, transaction::{DbTx, DbTxMut}, @@ -199,7 +200,7 @@ mod tests { random_block, random_block_range, random_contract_account_range, random_transition_range, }; use reth_primitives::{keccak256, Account, Address, SealedBlock, StorageEntry, H256, U256}; - use std::collections::BTreeMap; + use std::{collections::BTreeMap, ops::Deref}; stage_test_suite_ext!(MerkleTestRunner, merkle); @@ -262,8 +263,8 @@ mod tests { fn create_trie_loader<'tx, 'db>( tx: &'tx Transaction<'db, Env>, - ) -> DBTrieLoader<'tx, 'db, Env> { - DBTrieLoader::>::new(tx) + ) -> DBTrieLoader<'tx, as DatabaseGAT<'db>>::TXMut> { + DBTrieLoader::new(tx.deref()) } struct MerkleTestRunner { diff --git a/crates/storage/provider/Cargo.toml b/crates/storage/provider/Cargo.toml index 0a1802d14e..c0e26f2879 100644 --- a/crates/storage/provider/Cargo.toml +++ b/crates/storage/provider/Cargo.toml @@ -13,8 +13,8 @@ reth-primitives = { path = "../../primitives" } reth-interfaces = { path = "../../interfaces" } reth-revm-primitives = { path = "../../revm/revm-primitives" } reth-db = { path = "../db" } -reth-tracing = {path = "../../tracing"} -reth-rlp = {path = "../../rlp"} +reth-tracing = { path = "../../tracing" } +reth-rlp = { path = "../../rlp" } revm-primitives = "1.0.0" diff --git a/crates/storage/provider/src/lib.rs b/crates/storage/provider/src/lib.rs index a980f2f15a..77882f42b4 100644 --- a/crates/storage/provider/src/lib.rs +++ b/crates/storage/provider/src/lib.rs @@ -23,7 +23,7 @@ pub use providers::{ LatestStateProviderRef, ShareableDatabase, }; -/// Merkle trie +/// Helper type for loading Merkle Patricia Trees from the database pub mod trie; /// Execution result diff --git a/crates/storage/provider/src/providers/state/historical.rs b/crates/storage/provider/src/providers/state/historical.rs index 214f0ba2eb..fbfc248da1 100644 --- a/crates/storage/provider/src/providers/state/historical.rs +++ b/crates/storage/provider/src/providers/state/historical.rs @@ -10,7 +10,7 @@ use reth_db::{ }; use reth_interfaces::Result; use reth_primitives::{ - Account, Address, Bytecode, StorageKey, StorageValue, TransitionId, H256, U256, + Account, Address, Bytecode, Bytes, StorageKey, StorageValue, TransitionId, H256, U256, }; use std::marker::PhantomData; @@ -119,6 +119,15 @@ impl<'a, 'b, TX: DbTx<'a>> StateProvider for HistoricalStateProviderRef<'a, 'b, fn bytecode_by_hash(&self, code_hash: H256) -> Result> { self.tx.get::(code_hash).map_err(Into::into) } + + /// Get account and storage proofs. + fn proof( + &self, + _address: Address, + _keys: &[H256], + ) -> Result<(Vec, H256, Vec>)> { + todo!("this should retrieve past state info and generate proof") + } } /// State provider for a given transition diff --git a/crates/storage/provider/src/providers/state/latest.rs b/crates/storage/provider/src/providers/state/latest.rs index 311f37e415..e600eacb29 100644 --- a/crates/storage/provider/src/providers/state/latest.rs +++ b/crates/storage/provider/src/providers/state/latest.rs @@ -1,10 +1,17 @@ use crate::{ - providers::state::macros::delegate_provider_impls, AccountProvider, BlockHashProvider, - StateProvider, + providers::state::macros::delegate_provider_impls, trie::DBTrieLoader, AccountProvider, + BlockHashProvider, StateProvider, +}; +use reth_db::{ + cursor::{DbCursorRO, DbDupCursorRO}, + tables, + transaction::DbTx, +}; +use reth_interfaces::{provider::ProviderError, Result}; +use reth_primitives::{ + keccak256, Account, Address, Bytecode, Bytes, StorageKey, StorageValue, H256, KECCAK_EMPTY, + U256, }; -use reth_db::{cursor::DbDupCursorRO, tables, transaction::DbTx}; -use reth_interfaces::Result; -use reth_primitives::{Account, Address, Bytecode, StorageKey, StorageValue, H256, U256}; use std::marker::PhantomData; /// State provider over latest state that takes tx reference. @@ -52,6 +59,42 @@ impl<'a, 'b, TX: DbTx<'a>> StateProvider for LatestStateProviderRef<'a, 'b, TX> fn bytecode_by_hash(&self, code_hash: H256) -> Result> { self.db.get::(code_hash).map_err(Into::into) } + + fn proof( + &self, + address: Address, + keys: &[H256], + ) -> Result<(Vec, H256, Vec>)> { + let hashed_address = keccak256(address); + let loader = DBTrieLoader::new(self.db); + let root = self + .db + .cursor_read::()? + .last()? + .ok_or(ProviderError::Header { number: 0 })? + .1 + .state_root; + + let (account_proof, storage_root) = loader + .generate_acount_proof(self.db, root, hashed_address) + .map_err(|_| ProviderError::StateTree)?; + let account_proof = account_proof.into_iter().map(Bytes::from).collect(); + + let storage_proof = if storage_root == KECCAK_EMPTY { + // if there isn't storage, we return empty storage proofs + (0..keys.len()).map(|_| Vec::new()).collect() + } else { + let hashed_keys: Vec = keys.iter().map(keccak256).collect(); + loader + .generate_storage_proofs(self.db, storage_root, hashed_address, &hashed_keys) + .map_err(|_| ProviderError::StateTree)? + .into_iter() + .map(|v| v.into_iter().map(Bytes::from).collect()) + .collect() + }; + + Ok((account_proof, storage_root, storage_proof)) + } } /// State provider for the latest state. diff --git a/crates/storage/provider/src/providers/state/macros.rs b/crates/storage/provider/src/providers/state/macros.rs index d488010d71..ab4ae7b7bf 100644 --- a/crates/storage/provider/src/providers/state/macros.rs +++ b/crates/storage/provider/src/providers/state/macros.rs @@ -5,7 +5,7 @@ /// /// Used to implement provider traits. macro_rules! delegate_impls_to_as_ref { - (for $target:ty => $($trait:ident $(where [$($generics:tt)*])? { $(fn $func:ident(&self, $($arg:ident: $argty:path),*) -> $ret:path;)* })* ) => { + (for $target:ty => $($trait:ident $(where [$($generics:tt)*])? { $(fn $func:ident(&self, $($arg:ident: $argty:ty),*) -> $ret:path;)* })* ) => { $( impl<'a, $($($generics)*)?> $trait for $target { @@ -38,6 +38,7 @@ macro_rules! delegate_provider_impls { } StateProvider $(where [$($generics)*])?{ fn storage(&self, account: reth_primitives::Address, storage_key: reth_primitives::StorageKey) -> reth_interfaces::Result>; + fn proof(&self, address: reth_primitives::Address, keys: &[reth_primitives::H256]) -> reth_interfaces::Result<(Vec, reth_primitives::H256, Vec>)>; fn bytecode_by_hash(&self, code_hash: reth_primitives::H256) -> reth_interfaces::Result>; } ); diff --git a/crates/storage/provider/src/test_utils/mock.rs b/crates/storage/provider/src/test_utils/mock.rs index 31430ae84a..83e5885ea2 100644 --- a/crates/storage/provider/src/test_utils/mock.rs +++ b/crates/storage/provider/src/test_utils/mock.rs @@ -252,6 +252,14 @@ impl StateProvider for MockEthProvider { } })) } + + fn proof( + &self, + _address: Address, + _keys: &[H256], + ) -> Result<(Vec, H256, Vec>)> { + todo!() + } } impl EvmEnvProvider for MockEthProvider { diff --git a/crates/storage/provider/src/test_utils/noop.rs b/crates/storage/provider/src/test_utils/noop.rs index c7bd3f8020..d79970eaa7 100644 --- a/crates/storage/provider/src/test_utils/noop.rs +++ b/crates/storage/provider/src/test_utils/noop.rs @@ -4,8 +4,9 @@ use crate::{ }; use reth_interfaces::Result; use reth_primitives::{ - Account, Address, Block, BlockHash, BlockId, BlockNumber, Bytecode, ChainInfo, Header, Receipt, - StorageKey, StorageValue, TransactionSigned, TxHash, TxNumber, H256, U256, + Account, Address, Block, BlockHash, BlockId, BlockNumber, Bytecode, Bytes, ChainInfo, Header, + Receipt, StorageKey, StorageValue, TransactionSigned, TxHash, TxNumber, H256, KECCAK_EMPTY, + U256, }; use revm_primitives::{BlockEnv, CfgEnv}; use std::ops::RangeBounds; @@ -113,6 +114,14 @@ impl StateProvider for NoopProvider { fn bytecode_by_hash(&self, _code_hash: H256) -> Result> { Ok(None) } + + fn proof( + &self, + _address: Address, + _keys: &[H256], + ) -> Result<(Vec, H256, Vec>)> { + Ok((vec![], KECCAK_EMPTY, vec![])) + } } impl EvmEnvProvider for NoopProvider { diff --git a/crates/storage/provider/src/traits/state.rs b/crates/storage/provider/src/traits/state.rs index f82f2ad548..e2f502593c 100644 --- a/crates/storage/provider/src/traits/state.rs +++ b/crates/storage/provider/src/traits/state.rs @@ -3,18 +3,23 @@ use crate::BlockHashProvider; use auto_impl::auto_impl; use reth_interfaces::Result; use reth_primitives::{ - Address, BlockHash, BlockNumber, Bytecode, StorageKey, StorageValue, H256, KECCAK_EMPTY, U256, + Address, BlockHash, BlockNumber, Bytecode, Bytes, StorageKey, StorageValue, H256, KECCAK_EMPTY, + U256, }; /// An abstraction for a type that provides state data. #[auto_impl(&, Box)] pub trait StateProvider: BlockHashProvider + AccountProvider + Send + Sync { - /// Get storage. + /// Get storage of given account. fn storage(&self, account: Address, storage_key: StorageKey) -> Result>; /// Get account code by its hash fn bytecode_by_hash(&self, code_hash: H256) -> Result>; + /// Get account and storage proofs. + fn proof(&self, address: Address, keys: &[H256]) + -> Result<(Vec, H256, Vec>)>; + /// Get account code by its address. /// /// Returns `None` if the account doesn't exist or account is not a contract diff --git a/crates/storage/provider/src/transaction.rs b/crates/storage/provider/src/transaction.rs index a1bf8bbdf1..0c1d39b362 100644 --- a/crates/storage/provider/src/transaction.rs +++ b/crates/storage/provider/src/transaction.rs @@ -308,7 +308,7 @@ where // merkle tree { let current_root = self.get_header(parent_block_number)?.state_root; - let loader = DBTrieLoader::::new(self); + let loader = DBTrieLoader::new(self.deref_mut()); let root = loader.update_root(current_root, from..to)?; if root != block.state_root { return Err(TransactionError::StateTrieRootMismatch { diff --git a/crates/storage/provider/src/trie/mod.rs b/crates/storage/provider/src/trie/mod.rs index 1821aa7e16..ffb9661d18 100644 --- a/crates/storage/provider/src/trie/mod.rs +++ b/crates/storage/provider/src/trie/mod.rs @@ -2,7 +2,6 @@ use cita_trie::{PatriciaTrie, Trie}; use hasher::HasherKeccak; use reth_db::{ cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO}, - database::{Database, DatabaseGAT}, models::{AccountBeforeTx, TransitionIdAddress}, tables, transaction::{DbTx, DbTxMut}, @@ -18,6 +17,7 @@ use reth_rlp::{ use reth_tracing::tracing::*; use std::{ collections::{BTreeMap, BTreeSet}, + marker::PhantomData, ops::Range, sync::Arc, }; @@ -26,24 +26,28 @@ use std::{ #[allow(missing_docs)] #[derive(Debug, thiserror::Error)] pub enum TrieError { + /// Error returned by the underlying implementation. #[error("Some error occurred: {0}")] InternalError(#[from] cita_trie::TrieError), + /// The database doesn't contain the root of the trie. #[error("The root node wasn't found in the DB")] MissingRoot(H256), + /// Error returned by the database. #[error("{0:?}")] DatabaseError(#[from] reth_db::Error), + /// Error when encoding/decoding a value. #[error("{0:?}")] DecodeError(#[from] DecodeError), } -/// Database wrapper implementing HashDB trait. -pub struct HashDatabase<'tx, 'db, DB: Database> { - tx: &'tx >::TXMut, +/// Database wrapper implementing HashDB trait, with a read-write transaction. +pub struct HashDatabaseMut<'tx, TX> { + tx: &'tx TX, } -impl<'tx, 'db, DB> cita_trie::DB for HashDatabase<'tx, 'db, DB> +impl<'tx, 'db, TX> cita_trie::DB for HashDatabaseMut<'tx, TX> where - DB: Database, + TX: DbTxMut<'db> + DbTx<'db> + Send + Sync, { type Error = TrieError; @@ -87,9 +91,12 @@ where } } -impl<'tx, 'db, DB: Database> HashDatabase<'tx, 'db, DB> { +impl<'tx, 'db, TX> HashDatabaseMut<'tx, TX> +where + TX: DbTxMut<'db> + DbTx<'db> + Send + Sync, +{ /// Instantiates a new Database for the accounts trie, with an empty root - pub fn new(tx: &'tx >::TXMut) -> Result { + pub fn new(tx: &'tx TX) -> Result { let root = EMPTY_ROOT; if tx.get::(root)?.is_none() { tx.put::(root, [EMPTY_STRING_CODE].to_vec())?; @@ -98,10 +105,7 @@ impl<'tx, 'db, DB: Database> HashDatabase<'tx, 'db, DB> { } /// Instantiates a new Database for the accounts trie, with an existing root - pub fn from_root( - tx: &'tx >::TXMut, - root: H256, - ) -> Result { + pub fn from_root(tx: &'tx TX, root: H256) -> Result { if root == EMPTY_ROOT { return Self::new(tx) } @@ -110,15 +114,15 @@ impl<'tx, 'db, DB: Database> HashDatabase<'tx, 'db, DB> { } } -/// Database wrapper implementing HashDB trait. -pub struct DupHashDatabase<'tx, 'db, DB: Database> { - tx: &'tx >::TXMut, +/// Database wrapper implementing HashDB trait, with a read-write transaction. +pub struct DupHashDatabaseMut<'tx, TX> { + tx: &'tx TX, key: H256, } -impl<'tx, 'db, DB> cita_trie::DB for DupHashDatabase<'tx, 'db, DB> +impl<'tx, 'db, TX> cita_trie::DB for DupHashDatabaseMut<'tx, TX> where - DB: Database, + TX: DbTxMut<'db> + DbTx<'db> + Send + Sync, { type Error = TrieError; @@ -172,9 +176,12 @@ where } } -impl<'tx, 'db, DB: Database> DupHashDatabase<'tx, 'db, DB> { +impl<'tx, 'db, TX> DupHashDatabaseMut<'tx, TX> +where + TX: DbTxMut<'db> + DbTx<'db> + Send + Sync, +{ /// Instantiates a new Database for the storage trie, with an empty root - pub fn new(tx: &'tx >::TXMut, key: H256) -> Result { + pub fn new(tx: &'tx TX, key: H256) -> Result { let root = EMPTY_ROOT; let mut cursor = tx.cursor_dup_write::()?; if cursor.seek_by_key_subkey(key, root)?.filter(|entry| entry.hash == root).is_none() { @@ -187,11 +194,7 @@ impl<'tx, 'db, DB: Database> DupHashDatabase<'tx, 'db, DB> { } /// Instantiates a new Database for the storage trie, with an existing root - pub fn from_root( - tx: &'tx >::TXMut, - key: H256, - root: H256, - ) -> Result { + pub fn from_root(tx: &'tx TX, key: H256, root: H256) -> Result { if root == EMPTY_ROOT { return Self::new(tx, key) } @@ -203,6 +206,95 @@ impl<'tx, 'db, DB: Database> DupHashDatabase<'tx, 'db, DB> { } } +/// Database wrapper implementing HashDB trait, with a read-only transaction. +struct HashDatabase<'tx, 'itx, TX: DbTx<'itx>> { + tx: &'tx TX, + _p: PhantomData<&'itx ()>, // to suppress "unused" lifetime 'itx +} + +impl<'tx, 'itx, TX> cita_trie::DB for HashDatabase<'tx, 'itx, TX> +where + TX: DbTx<'itx>, +{ + type Error = TrieError; + + fn get(&self, key: &[u8]) -> Result>, Self::Error> { + Ok(self.tx.get::(H256::from_slice(key))?) + } + + fn contains(&self, key: &[u8]) -> Result { + Ok(::get(self, key)?.is_some()) + } + + fn insert(&self, _key: Vec, _value: Vec) -> Result<(), Self::Error> { + // this could be avoided if cita_trie::DB was split into two traits + // with read and write operations respectively + unimplemented!("insert isn't valid for read-only transaction"); + } + + fn remove(&self, _key: &[u8]) -> Result<(), Self::Error> { + unimplemented!("remove isn't valid for read-only transaction"); + } + + fn flush(&self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl<'tx, 'itx, TX: DbTx<'itx>> HashDatabase<'tx, 'itx, TX> { + /// Instantiates a new Database for the accounts trie, with an existing root + fn from_root(tx: &'tx TX, root: H256) -> Result { + tx.get::(root)?.ok_or(TrieError::MissingRoot(root))?; + Ok(Self { tx, _p: Default::default() }) + } +} + +/// Database wrapper implementing HashDB trait, with a read-only transaction. +struct DupHashDatabase<'tx, 'itx, TX: DbTx<'itx>> { + tx: &'tx TX, + key: H256, + _p: PhantomData<&'itx ()>, // to suppress "unused" lifetime 'itx +} + +impl<'tx, 'itx, TX> cita_trie::DB for DupHashDatabase<'tx, 'itx, TX> +where + TX: DbTx<'itx>, +{ + type Error = TrieError; + + fn get(&self, key: &[u8]) -> Result>, Self::Error> { + let mut cursor = self.tx.cursor_dup_read::()?; + Ok(cursor.seek_by_key_subkey(self.key, H256::from_slice(key))?.map(|entry| entry.node)) + } + + fn contains(&self, key: &[u8]) -> Result { + Ok(::get(self, key)?.is_some()) + } + + fn insert(&self, _key: Vec, _value: Vec) -> Result<(), Self::Error> { + // Caching and bulk inserting shouldn't be needed, as the data is ordered + unimplemented!("insert isn't valid for read-only transaction"); + } + + fn remove(&self, _key: &[u8]) -> Result<(), Self::Error> { + unimplemented!("remove isn't valid for read-only transaction"); + } + + fn flush(&self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl<'tx, 'itx, TX: DbTx<'itx>> DupHashDatabase<'tx, 'itx, TX> { + /// Instantiates a new Database for the storage trie, with an existing root + fn from_root(tx: &'tx TX, key: H256, root: H256) -> Result { + tx.cursor_dup_read::()? + .seek_by_key_subkey(key, root)? + .ok_or(TrieError::MissingRoot(root))?; + Ok(Self { tx, key, _p: Default::default() }) + } +} + /// An Ethereum account, for RLP encoding traits deriving. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)] pub struct EthAccount { @@ -240,18 +332,28 @@ impl EthAccount { } } +/// A merkle proof of existence (or nonexistence) of a leaf value. Consists +/// of a the encoded nodes in the path from the root of the tree to the leaf. +pub type MerkleProof = Vec>; + /// Struct for calculating the root of a merkle patricia tree, /// while populating the database with intermediate hashes. -pub struct DBTrieLoader<'tx, 'db, DB: Database> { - tx: &'tx >::TXMut, +pub struct DBTrieLoader<'tx, TX> { + tx: &'tx TX, } -impl<'tx, 'db, DB: Database> DBTrieLoader<'tx, 'db, DB> { +impl<'tx, TX> DBTrieLoader<'tx, TX> { /// Create new instance of trie loader. - pub fn new(tx: &'tx >::TXMut) -> Self { + pub fn new(tx: &'tx TX) -> Self { Self { tx } } +} +// Read-write impls +impl<'tx, 'db, TX> DBTrieLoader<'tx, TX> +where + TX: DbTxMut<'db> + DbTx<'db> + Send + Sync, +{ /// Calculates the root of the state trie, saving intermediate hashes in the database. pub fn calculate_root(&self) -> Result { self.tx.clear::()?; @@ -260,7 +362,7 @@ impl<'tx, 'db, DB: Database> DBTrieLoader<'tx, 'db, DB> { let mut accounts_cursor = self.tx.cursor_read::()?; let mut walker = accounts_cursor.walk(None)?; - let db = Arc::new(HashDatabase::::new(self.tx)?); + let db = Arc::new(HashDatabaseMut::new(self.tx)?); let hasher = Arc::new(HasherKeccak::new()); @@ -281,7 +383,7 @@ impl<'tx, 'db, DB: Database> DBTrieLoader<'tx, 'db, DB> { /// Calculate the accounts storage root. pub fn calculate_storage_root(&self, address: H256) -> Result { - let db = Arc::new(DupHashDatabase::::new(self.tx, address)?); + let db = Arc::new(DupHashDatabaseMut::new(self.tx, address)?); let hasher = Arc::new(HasherKeccak::new()); @@ -319,7 +421,7 @@ impl<'tx, 'db, DB: Database> DBTrieLoader<'tx, 'db, DB> { let changed_accounts = self.gather_changes(tid_range)?; - let db = Arc::new(HashDatabase::::from_root(self.tx, root)?); + let db = Arc::new(HashDatabaseMut::from_root(self.tx, root)?); let hasher = Arc::new(HasherKeccak::new()); @@ -362,7 +464,7 @@ impl<'tx, 'db, DB: Database> DBTrieLoader<'tx, 'db, DB> { address: H256, changed_storages: BTreeSet, ) -> Result { - let db = Arc::new(DupHashDatabase::::from_root(self.tx, address, root)?); + let db = Arc::new(DupHashDatabaseMut::from_root(self.tx, address, root)?); let hasher = Arc::new(HasherKeccak::new()); @@ -437,6 +539,52 @@ impl<'tx, 'db, DB: Database> DBTrieLoader<'tx, 'db, DB> { } } +// Read-only impls +impl<'tx, 'db, TX> DBTrieLoader<'tx, TX> +where + TX: DbTx<'db> + Send + Sync, +{ + /// Returns a Merkle proof of the given account, plus its storage root hash. + pub fn generate_acount_proof<'itx>( + &self, + tx: &'tx impl DbTx<'itx>, + root: H256, + address: H256, + ) -> Result<(MerkleProof, H256), TrieError> { + let db = Arc::new(HashDatabase::from_root(tx, root)?); + let hasher = Arc::new(HasherKeccak::new()); + + let trie = PatriciaTrie::from(Arc::clone(&db), Arc::clone(&hasher), root.as_bytes())?; + let proof = trie.get_proof(address.as_bytes())?; + + let Some(account) = trie.get(address.as_slice())? else { return Ok((proof, KECCAK_EMPTY)) }; + + let storage_root = EthAccount::decode(&mut account.as_slice())?.storage_root; + + Ok((proof, storage_root)) + } + + /// Returns a Merkle proof of the given storage keys, starting at the given root hash. + pub fn generate_storage_proofs<'itx>( + &self, + tx: &'tx impl DbTx<'itx>, + storage_root: H256, + address: H256, + keys: &[H256], + ) -> Result, TrieError> { + let db = Arc::new(DupHashDatabase::from_root(tx, address, storage_root)?); + let hasher = Arc::new(HasherKeccak::new()); + + let trie = + PatriciaTrie::from(Arc::clone(&db), Arc::clone(&hasher), storage_root.as_bytes())?; + + let proof = + keys.iter().map(|key| trie.get_proof(key.as_bytes())).collect::, _>>()?; + + Ok(proof) + } +} + #[cfg(test)] mod tests { use crate::Transaction; @@ -445,6 +593,7 @@ mod tests { use assert_matches::assert_matches; use proptest::{prelude::ProptestConfig, proptest}; use reth_db::{ + database::{Database, DatabaseGAT}, mdbx::{test_utils::create_test_rw_db, Env, WriteMap}, tables, transaction::DbTxMut, @@ -453,14 +602,43 @@ mod tests { hex_literal::hex, keccak256, proofs::{genesis_state_root, KeccakHasher, EMPTY_ROOT}, - Address, ChainSpec, MAINNET, + Address, Bytes, ChainSpec, Genesis, MAINNET, }; use std::{collections::HashMap, ops::Deref, str::FromStr}; use triehash::sec_trie_root; + fn load_mainnet_genesis_root(tx: &mut Transaction<'_, DB>) -> Genesis { + let ChainSpec { genesis, .. } = MAINNET.clone(); + + // Insert account state + for (address, account) in &genesis.alloc { + tx.put::( + *address, + Account { + nonce: account.nonce.unwrap_or_default(), + balance: account.balance, + bytecode_hash: None, + }, + ) + .unwrap(); + tx.put::( + keccak256(address), + Account { + nonce: account.nonce.unwrap_or_default(), + balance: account.balance, + bytecode_hash: None, + }, + ) + .unwrap(); + } + tx.commit().unwrap(); + + genesis + } + fn create_test_loader<'tx, 'db>( tx: &'tx Transaction<'db, Env>, - ) -> DBTrieLoader<'tx, 'db, Arc>> { + ) -> DBTrieLoader<'tx, > as DatabaseGAT<'db>>::TXMut> { DBTrieLoader::new(tx.deref()) } @@ -596,21 +774,8 @@ mod tests { fn verify_genesis() { let db = create_test_rw_db(); let mut tx = Transaction::new(db.as_ref()).unwrap(); - let ChainSpec { genesis, .. } = MAINNET.clone(); - // Insert account state - for (address, account) in &genesis.alloc { - tx.put::( - keccak256(address), - Account { - nonce: account.nonce.unwrap_or_default(), - balance: account.balance, - bytecode_hash: None, - }, - ) - .unwrap(); - } - tx.commit().unwrap(); + let genesis = load_mainnet_genesis_root(&mut tx); let state_root = genesis_state_root(&genesis.alloc); @@ -708,4 +873,125 @@ mod tests { test_with_accounts(accounts); }); } + + #[test] + fn get_proof() { + let db = create_test_rw_db(); + let mut tx = Transaction::new(db.as_ref()).unwrap(); + + load_mainnet_genesis_root(&mut tx); + + let root = { + let trie = create_test_loader(&tx); + trie.calculate_root().expect("should be able to load trie") + }; + + tx.commit().unwrap(); + + let address = Address::from(hex!("000d836201318ec6899a67540690382780743280")); + + let trie = create_test_loader(&tx); + let (proof, storage_root) = trie + .generate_acount_proof(&tx.inner().tx().unwrap(), root, keccak256(address)) + .expect("failed to generate proof"); + + // values extracted from geth via rpc: + // { + // "method": "eth_getProof", + // "params": ["0x000d836201318ec6899a67540690382780743280", [], "0x0"] + // } + let expected = [ + hex!("f90211a090dcaf88c40c7bbc95a912cbdde67c175767b31173df9ee4b0d733bfdd511c43a0babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bda0473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021a0bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0a04e44caecff45c9891f74f6a2156735886eedf6f1a733628ebc802ec79d844648a0a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7a0e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92da0f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721a07117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681a069eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472a0203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8ea09287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208a06fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94eca07b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475a051f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0a089d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb80").as_slice(), + hex!("f90211a0dae48f5b47930c28bb116fbd55e52cd47242c71bf55373b55eb2805ee2e4a929a00f1f37f337ec800e2e5974e2e7355f10f1a4832b39b846d916c3597a460e0676a0da8f627bb8fbeead17b318e0a8e4f528db310f591bb6ab2deda4a9f7ca902ab5a0971c662648d58295d0d0aa4b8055588da0037619951217c22052802549d94a2fa0ccc701efe4b3413fd6a61a6c9f40e955af774649a8d9fd212d046a5a39ddbb67a0d607cdb32e2bd635ee7f2f9e07bc94ddbd09b10ec0901b66628e15667aec570ba05b89203dc940e6fa70ec19ad4e01d01849d3a5baa0a8f9c0525256ed490b159fa0b84227d48df68aecc772939a59afa9e1a4ab578f7b698bdb1289e29b6044668ea0fd1c992070b94ace57e48cbf6511a16aa770c645f9f5efba87bbe59d0a042913a0e16a7ccea6748ae90de92f8aef3b3dc248a557b9ac4e296934313f24f7fced5fa042373cf4a00630d94de90d0a23b8f38ced6b0f7cb818b8925fee8f0c2a28a25aa05f89d2161c1741ff428864f7889866484cef622de5023a46e795dfdec336319fa07597a017664526c8c795ce1da27b8b72455c49657113e0455552dbc068c5ba31a0d5be9089012fda2c585a1b961e988ea5efcd3a06988e150a8682091f694b37c5a0f7b0352e38c315b2d9a14d51baea4ddee1770974c806e209355233c3c89dce6ea049bf6e8df0acafd0eff86defeeb305568e44d52d2235cf340ae15c6034e2b24180").as_slice(), + hex!("f901f1a0cf67e0f5d5f8d70e53a6278056a14ddca46846f5ef69c7bde6810d058d4a9eda80a06732ada65afd192197fe7ce57792a7f25d26978e64e954b7b84a1f7857ac279da05439f8d011683a6fc07efb90afca198fd7270c795c835c7c85d91402cda992eaa0449b93033b6152d289045fdb0bf3f44926f831566faa0e616b7be1abaad2cb2da031be6c3752bcd7afb99b1bb102baf200f8567c394d464315323a363697646616a0a40e3ed11d906749aa501279392ffde868bd35102db41364d9c601fd651f974aa0044bfa4fe8dd1a58e6c7144da79326e94d1331c0b00373f6ae7f3662f45534b7a098005e3e48db68cb1dc9b9f034ff74d2392028ddf718b0f2084133017da2c2e7a02a62bc40414ee95b02e202a9e89babbabd24bef0abc3fc6dcd3e9144ceb0b725a0239facd895bbf092830390a8676f34b35b29792ae561f196f86614e0448a5792a0a4080f88925daff6b4ce26d188428841bd65655d8e93509f2106020e76d41eefa04918987904be42a6894256ca60203283d1b89139cf21f09f5719c44b8cdbb8f7a06201fc3ef0827e594d953b5e3165520af4fceb719e11cc95fd8d3481519bfd8ca05d0e353d596bd725b09de49c01ede0f29023f0153d7b6d401556aeb525b2959ba0cd367d0679950e9c5f2aa4298fd4b081ade2ea429d71ff390c50f8520e16e30880").as_slice(), + hex!("f87180808080808080a0dbee8b33c73b86df839f309f7ac92eee19836e08b39302ffa33921b3c6a09f66a06068b283d51aeeee682b8fb5458354315d0b91737441ede5e137c18b4775174a8080808080a0fe7779c7d58c2fda43eba0a6644043c86ebb9ceb4836f89e30831f23eb059ece8080").as_slice(), + hex!("f8719f20b71c90b0d523dd5004cf206f325748da347685071b34812e21801f5270c4b84ff84d80890ad78ebc5ac6200000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").as_slice(), + ]; + + assert_eq!(storage_root, EMPTY_ROOT); + + assert_eq!(proof.len(), 5); + + for (node, expected) in proof.into_iter().zip(expected.into_iter()) { + assert_eq!(Bytes::from(node.as_slice()), Bytes::from(expected)); + } + } + + #[test] + fn get_storage_proofs() { + let db = create_test_rw_db(); + let mut tx = Transaction::new(db.as_ref()).unwrap(); + + let address = Address::from_str("9fe4abd71ad081f091bd06dd1c16f7e92927561e").unwrap(); + let hashed_address = keccak256(address); + + let storage = HashMap::from([ + (H256::zero(), U256::from(3)), + (H256::from_low_u64_be(2), U256::from(1)), + ]); + + let code = "el buen fla"; + let account = Account { + nonce: 155, + balance: U256::from(414241124u32), + bytecode_hash: Some(keccak256(code)), + }; + tx.put::(hashed_address, account).unwrap(); + + for (k, v) in storage.clone() { + tx.put::( + hashed_address, + StorageEntry { key: keccak256(k), value: v }, + ) + .unwrap(); + } + + let root = { + let trie = create_test_loader(&tx); + trie.calculate_root().expect("should be able to load trie") + }; + + tx.commit().unwrap(); + + let trie = create_test_loader(&tx); + let (account_proof, storage_root) = trie + .generate_acount_proof(&tx.inner().tx().unwrap(), root, hashed_address) + .expect("failed to generate proof"); + + // values extracted from geth via rpc: + let expected_account = hex!("f86fa1205126413e7857595763591580306b3f228f999498c4c5dfa74f633364936e7651b84bf849819b8418b0d164a029ff6f4d518044318d75b118cf439d8d3d7249c8afcba06ba9ecdf8959410571a02ce1a85814ad94a94ed2a1abaf7c57e9b64326622c1b8c21b4ba4d0e7df61392").as_slice(); + let expected_storage = [ + [ + // 0x0000000000000000000000000000000000000000000000000000000000000002 + hex!("f8518080a04355bd3061ad2d17e0782413925b4fd81a56bd162d91eedb2a00d6c87611471480a015503e91f9250654cf72906e38a7cb14c3f1cc06658379d37f0c5b5c32482880808080808080808080808080").as_slice(), + hex!("e2a0305787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace01").as_slice(), + ], + [ + // 0x0000000000000000000000000000000000000000000000000000000000000000 + hex!("f8518080a04355bd3061ad2d17e0782413925b4fd81a56bd162d91eedb2a00d6c87611471480a015503e91f9250654cf72906e38a7cb14c3f1cc06658379d37f0c5b5c32482880808080808080808080808080").as_slice(), + hex!("e2a0390decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56303").as_slice(), + ] + ]; + + assert!(storage_root != EMPTY_ROOT); + + assert_eq!(account_proof.len(), 1); + assert_eq!(account_proof[0], expected_account); + + let storage_proofs = trie + .generate_storage_proofs( + &tx.inner().tx().unwrap(), + storage_root, + hashed_address, + &[keccak256(H256::from_low_u64_be(2)), keccak256(H256::zero())], + ) + .expect("couldn't generate storage proof"); + + for (proof, expected) in storage_proofs.into_iter().zip(expected_storage) { + assert_eq!(proof.len(), expected.len()); + for (got_node, expected_node) in proof.into_iter().zip(expected) { + assert_eq!(got_node, expected_node); + } + } + } }