Skip to content
1 change: 1 addition & 0 deletions eth-trie.rs/src/trie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Vec<u8>>> {
self.commit()?;
let key_path = &Nibbles::from_raw(key, true);
let result = self.get_path_at(&self.root, key_path, 0);

Expand Down
67 changes: 57 additions & 10 deletions zilliqa/src/api/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand All @@ -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",
Expand Down Expand Up @@ -900,6 +903,57 @@ fn syncing(params: Params, node: &Arc<RwLock<Node>>) -> Result<SyncingResult> {
}
}

fn get_proof(params: Params, node: &Arc<RwLock<Node>>) -> Result<Proof> {
let mut params = params.sequence();
let address: Address = params.next()?;
let storage_keys: Vec<U256> = params.next()?;
let storage_keys = storage_keys
.into_iter()
.map(|key| B256::new(key.to_be_bytes()))
.collect::<Vec<_>>();
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<'_>,
Expand Down Expand Up @@ -1266,13 +1320,6 @@ fn get_filter_logs(params: Params, node: &Arc<RwLock<Node>>) -> Result<serde_jso
Err(anyhow!("filter not found"))
}
}

/// eth_getProof
/// Returns the account and storage values of the specified account including the Merkle-proof.
fn get_proof(_params: Params, _node: &Arc<RwLock<Node>>) -> 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<RwLock<Node>>) -> Result<()> {
Expand Down
28 changes: 28 additions & 0 deletions zilliqa/src/api/types/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
#[serde(serialize_with = "vec_hex")]
pub proof: Vec<Vec<u8>>,
}

#[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<Vec<u8>>,
#[serde(rename = "storageProof")]
pub storage_proof: Vec<StorageProof>,
}

#[cfg(test)]
mod tests {
use alloy::primitives::B256;
Expand Down
50 changes: 50 additions & 0 deletions zilliqa/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ use crate::{
transaction::EvmGas,
};

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct StorageProof {
pub key: B256,
pub value: Vec<u8>,
pub proof: Vec<Vec<u8>>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Proof {
pub account: Account,
pub account_proof: Vec<Vec<u8>>,
pub storage_proofs: Vec<StorageProof>,
}

#[derive(Clone, Debug)]
/// The state of the blockchain, consisting of:
/// - state - a database of Map<Address, Map<key,value>>
Expand Down Expand Up @@ -442,6 +455,43 @@ impl State {
)?)
}

pub fn get_proof(&mut self, address: Address, storage_keys: &[B256]) -> Result<Proof> {
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<Option<Block>> {
self.sql.get_block(BlockFilter::Height(number))
}
Expand Down
119 changes: 117 additions & 2 deletions zilliqa/tests/it/eth.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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};

Expand Down Expand Up @@ -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::<Vec<_>>();

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::<Vec<_>>();

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");
Expand Down
Loading