diff --git a/eth-trie.rs/src/trie.rs b/eth-trie.rs/src/trie.rs index d898480541..975d5733e3 100644 --- a/eth-trie.rs/src/trie.rs +++ b/eth-trie.rs/src/trie.rs @@ -383,6 +383,7 @@ where /// nodes of the longest existing prefix of the key (at least the root node), ending /// with the node that proves the absence of the key. fn get_proof(&mut self, key: &[u8]) -> TrieResult>> { + self.commit()?; let key_path = &Nibbles::from_raw(key, true); let result = self.get_path_at(&self.root, key_path, 0); diff --git a/zilliqa/src/api/eth.rs b/zilliqa/src/api/eth.rs index e16dcb80b9..cc0f13257d 100644 --- a/zilliqa/src/api/eth.rs +++ b/zilliqa/src/api/eth.rs @@ -24,7 +24,7 @@ use jsonrpsee::{ }, }; use parking_lot::{RwLock, RwLockReadGuard}; -use revm::primitives::keccak256; +use revm::primitives::{Bytecode, keccak256}; use serde_json::json; use tracing::*; @@ -36,7 +36,10 @@ use super::{ }, }; use crate::{ - api::{types::eth::GetAccountResult, zilliqa::ZilAddress}, + api::{ + types::eth::{GetAccountResult, Proof, StorageProof}, + zilliqa::ZilAddress, + }, cfg::EnabledApi, constants::BASE_FEE_PER_GAS, crypto::Hash, @@ -69,6 +72,7 @@ pub fn rpc_module( ("eth_gasPrice", get_gas_price), ("eth_getAccount", get_account), ("eth_getBalance", get_balance), + ("eth_getProof", get_proof), ("eth_getBlockByHash", get_block_by_hash), ("eth_getBlockByNumber", get_block_by_number), ("eth_getBlockReceipts", get_block_receipts), @@ -84,7 +88,6 @@ pub fn rpc_module( ("eth_getFilterChanges", get_filter_changes), ("eth_getFilterLogs", get_filter_logs), ("eth_getLogs", get_logs), - ("eth_getProof", get_proof), ("eth_getStorageAt", get_storage_at), ( "eth_getTransactionByBlockHashAndIndex", @@ -900,6 +903,57 @@ fn syncing(params: Params, node: &Arc>) -> Result { } } +fn get_proof(params: Params, node: &Arc>) -> Result { + let mut params = params.sequence(); + let address: Address = params.next()?; + let storage_keys: Vec = params.next()?; + let storage_keys = storage_keys + .into_iter() + .map(|key| B256::new(key.to_be_bytes())) + .collect::>(); + let block_id: BlockId = params.next()?; + expect_end_of_params(&mut params, 3, 3)?; + + let block = node.read().get_block(block_id)?; + + let block = build_errored_response_for_missing_block(block_id, block)?; + + let mut state = node + .read() + .consensus + .state() + .at_root(block.state_root_hash().into()); + let computed_proof = state.get_proof(address, &storage_keys)?; + + let acc_code = Bytecode::new_raw( + computed_proof + .account + .code + .evm_code() + .unwrap_or_default() + .into(), + ); + + Ok(Proof { + address, + account_proof: computed_proof.account_proof, + storage_proof: computed_proof + .storage_proofs + .into_iter() + .map(|single_item| StorageProof { + proof: single_item.proof, + key: single_item.key, + value: single_item.value, + }) + .collect(), + nonce: computed_proof.account.nonce, + balance: computed_proof.account.balance, + storage_hash: computed_proof.account.storage_root, + code_hash: acc_code.hash_slow(), + }) +} + +#[allow(clippy::redundant_allocation)] #[allow(clippy::redundant_allocation, clippy::await_holding_lock)] async fn subscribe( params: Params<'_>, @@ -1266,13 +1320,6 @@ fn get_filter_logs(params: Params, node: &Arc>) -> Result>) -> Result<()> { - Err(anyhow!("API method eth_getProof is not implemented yet")) -} - /// eth_hashrate /// Returns the number of hashes per second that the node is mining with. fn hashrate(_params: Params, _node: &Arc>) -> Result<()> { diff --git a/zilliqa/src/api/types/eth.rs b/zilliqa/src/api/types/eth.rs index bd8b0b2fcf..ef7774b7e8 100644 --- a/zilliqa/src/api/types/eth.rs +++ b/zilliqa/src/api/types/eth.rs @@ -517,6 +517,34 @@ pub struct GetAccountResult { pub storage_root: B256, } +#[derive(Debug, Clone, Serialize)] +pub struct StorageProof { + #[serde(serialize_with = "hex")] + pub key: B256, + #[serde(serialize_with = "hex")] + pub value: Vec, + #[serde(serialize_with = "vec_hex")] + pub proof: Vec>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Proof { + #[serde(serialize_with = "hex")] + pub address: Address, + #[serde(serialize_with = "hex")] + pub balance: u128, + #[serde(rename = "codeHash", serialize_with = "hex")] + pub code_hash: B256, + #[serde(serialize_with = "hex")] + pub nonce: u64, + #[serde(rename = "storageHash", serialize_with = "hex")] + pub storage_hash: B256, + #[serde(rename = "accountProof", serialize_with = "vec_hex")] + pub account_proof: Vec>, + #[serde(rename = "storageProof")] + pub storage_proof: Vec, +} + #[cfg(test)] mod tests { use alloy::primitives::B256; diff --git a/zilliqa/src/state.rs b/zilliqa/src/state.rs index c9bccc63b8..66d7945f0b 100644 --- a/zilliqa/src/state.rs +++ b/zilliqa/src/state.rs @@ -29,6 +29,19 @@ use crate::{ transaction::EvmGas, }; +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct StorageProof { + pub key: B256, + pub value: Vec, + pub proof: Vec>, +} +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Proof { + pub account: Account, + pub account_proof: Vec>, + pub storage_proofs: Vec, +} + #[derive(Clone, Debug)] /// The state of the blockchain, consisting of: /// - state - a database of Map> @@ -442,6 +455,43 @@ impl State { )?) } + pub fn get_proof(&mut self, address: Address, storage_keys: &[B256]) -> Result { + if !self.has_account(address)? { + return Ok(Proof::default()); + }; + + // get_proof() requires &mut so clone state and don't mutate the origin + let account = self.get_account(address)?; + + let account_proof = self + .accounts + .lock() + .unwrap() + .get_proof(Self::account_key(address).as_slice())?; + + let mut storage_trie = self.get_account_trie(address)?; + storage_trie.root_hash()?; + + let storage_proofs = { + let mut storage_proofs = Vec::new(); + for key in storage_keys { + let key = Self::account_storage_key(address, *key); + let Some(value) = storage_trie.get(key.as_slice())? else { + continue; + }; + let proof = storage_trie.get_proof(key.as_slice())?; + storage_proofs.push(StorageProof { proof, key, value }); + } + storage_proofs + }; + + Ok(Proof { + account, + account_proof, + storage_proofs, + }) + } + pub fn get_canonical_block_by_number(&self, number: u64) -> Result> { self.sql.get_block(BlockFilter::Height(number)) } diff --git a/zilliqa/tests/it/eth.rs b/zilliqa/tests/it/eth.rs index 848cd10f78..597671f1a8 100644 --- a/zilliqa/tests/it/eth.rs +++ b/zilliqa/tests/it/eth.rs @@ -1,6 +1,7 @@ -use std::{fmt::Debug, ops::DerefMut}; +use std::{fmt::Debug, ops::DerefMut, sync::Arc}; -use alloy::primitives::{Address, hex}; +use alloy::primitives::{Address, B256, hex}; +use eth_trie::{EthTrie, MemoryDB, Trie}; use ethabi::{Token, ethereum_types::U64}; use ethers::{ abi::FunctionExt, @@ -20,6 +21,7 @@ use futures::{StreamExt, future::join_all}; use primitive_types::{H160, H256}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; +use zilliqa::state::{Account, State}; use crate::{LocalRpcClient, Network, Wallet, deploy_contract}; @@ -1554,6 +1556,119 @@ async fn get_block_receipts(mut network: Network) { assert!(receipts.contains(&individual1)); } +#[zilliqa_macros::test] +async fn test_eth_get_proof(mut network: Network) { + let wallet = network.genesis_wallet().await; + + // Example from https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getstorageat. + let (hash, _) = deploy_contract( + "tests/it/contracts/Storage.sol", + "Storage", + 0u128, + &wallet, + &mut network, + ) + .await; + + let receipt = wallet.get_transaction_receipt(hash).await.unwrap().unwrap(); + let contract_address = receipt.contract_address.unwrap(); + + let deployed_at_block = receipt.block_number.unwrap().as_u64(); + let deployed_at_block = wallet.get_block(deployed_at_block).await.unwrap().unwrap(); + + let contract_account = { + let node = network.nodes[0].inner.read(); + node.consensus + .state() + .get_account(Address::from(contract_address.0)) + .unwrap() + }; + + // A single storage item with slot = 0 + let storage_key = H256::from([0u8; 32]); + let storage_keys = vec![storage_key]; + let proof = wallet + .get_proof( + contract_address, + storage_keys, + Some(BlockNumber::from(deployed_at_block.number.unwrap()).into()), + ) + .await + .unwrap(); + + let storage_value = { + let node = network.nodes[0].inner.read(); + node.consensus + .state() + .get_account_storage( + Address::from(contract_address.0), + B256::from_slice(storage_key.as_bytes()), + ) + .unwrap() + }; + + // Verify account + { + let memdb = Arc::new(MemoryDB::new(true)); + let trie = EthTrie::new(Arc::clone(&memdb)); + + let account_proof = proof + .account_proof + .iter() + .map(|elem| elem.to_vec()) + .collect::>(); + + let verify_result = trie + .verify_proof( + B256::from_slice(deployed_at_block.state_root.as_bytes()), + State::account_key(Address::from(contract_address.0)).as_slice(), + account_proof, + ) + .unwrap() + .unwrap(); + + let recovered_account: Account = + bincode::serde::decode_from_slice(&verify_result, bincode::config::legacy()) + .unwrap() + .0; + assert_eq!(recovered_account.balance, 0); + assert_eq!( + recovered_account.storage_root, + contract_account.storage_root + ); + } + + // Verify storage key + { + let memdb = Arc::new(MemoryDB::new(true)); + let trie = EthTrie::new(Arc::clone(&memdb)); + + // There's only a single key we want to proove + let single_proof = proof.storage_proof.last().unwrap(); + + let storage_proof = single_proof + .proof + .iter() + .map(|elem| elem.to_vec()) + .collect::>(); + + let verify_result = trie + .verify_proof( + B256::from_slice(contract_account.storage_root.as_slice()), + State::account_storage_key( + Address::from(contract_address.0), + B256::from_slice(storage_key.as_bytes()), + ) + .as_slice(), + storage_proof, + ) + .unwrap() + .unwrap(); + + assert_eq!(verify_result.as_slice(), storage_value.as_slice()); + } +} + #[zilliqa_macros::test] async fn test_block_filter(mut network: Network) { println!("Starting block filter test");