diff --git a/eth-trie.rs/src/trie.rs b/eth-trie.rs/src/trie.rs index 8ce809cd3..66b03419d 100644 --- a/eth-trie.rs/src/trie.rs +++ b/eth-trie.rs/src/trie.rs @@ -380,6 +380,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 651044d56..b88d4efc9 100644 --- a/zilliqa/src/api/eth.rs +++ b/zilliqa/src/api/eth.rs @@ -23,6 +23,7 @@ use jsonrpsee::{ }, PendingSubscriptionSink, RpcModule, SubscriptionMessage, }; +use revm::primitives::Bytecode; use serde::Deserialize; use tracing::*; @@ -34,7 +35,10 @@ use super::{ }, }; use crate::{ - api::zilliqa::ZilAddress, + api::{ + types::eth::{Proof, StorageProof}, + zilliqa::ZilAddress, + }, cfg::EnabledApi, crypto::Hash, error::ensure_success, @@ -64,6 +68,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), @@ -79,7 +84,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", @@ -891,6 +895,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 node = node.lock().unwrap(); + + let block = node.get_block(block_id)?; + + let block = build_errored_response_for_missing_block(block_id, block)?; + + let state = node + .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)] async fn subscribe( params: Params<'_>, @@ -1046,12 +1101,6 @@ fn get_filter_logs(_params: Params, _node: &Arc>) -> Result<()> { todo!("Endpoint not implemented yet") } -/// eth_getProof -/// Returns the account and storage values of the specified account including the Merkle-proof. -fn get_proof(_params: Params, _node: &Arc>) -> Result<()> { - todo!("Endpoint 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 51523367d..b7139c9d5 100644 --- a/zilliqa/src/api/types/eth.rs +++ b/zilliqa/src/api/types/eth.rs @@ -474,6 +474,34 @@ pub enum SyncingResult { Struct(SyncingStruct), } +#[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 059efe9db..088ec949d 100644 --- a/zilliqa/src/state.rs +++ b/zilliqa/src/state.rs @@ -30,6 +30,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> @@ -382,6 +395,43 @@ impl State { &bincode::serialize(&account)?, )?) } + + pub fn get_proof(&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 mut state = self.clone(); + state.root_hash()?; + let account = state.get_account(address)?; + + let account_proof = state + .accounts + .get_proof(Self::account_key(address).as_slice())?; + + let mut storage_trie = state.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 mod contract_addr { diff --git a/zilliqa/tests/it/eth.rs b/zilliqa/tests/it/eth.rs index 6155ee4e3..b067a6148 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::{hex, Address}; +use alloy::primitives::{hex, Address, B256}; +use eth_trie::{EthTrie, MemoryDB, Trie}; use ethabi::{ethereum_types::U64, Token}; use ethers::{ abi::FunctionExt, @@ -19,6 +20,7 @@ use ethers::{ use futures::{future::join_all, StreamExt}; use primitive_types::{H160, H256}; use serde::{Deserialize, Serialize}; +use zilliqa::state::{Account, State}; use crate::{deploy_contract, LocalRpcClient, Network, Wallet}; @@ -1473,3 +1475,112 @@ 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", + &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.lock().unwrap(); + 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.lock().unwrap(); + 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 = bincode::deserialize::(&verify_result).unwrap(); + 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()); + } +}