Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Proof implementation #2128

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eth-trie.rs/src/trie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
65 changes: 57 additions & 8 deletions zilliqa/src/api/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use jsonrpsee::{
},
PendingSubscriptionSink, RpcModule, SubscriptionMessage,
};
use revm::primitives::Bytecode;
use serde::Deserialize;
use tracing::*;

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

fn get_proof(params: Params, node: &Arc<Mutex<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 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<'_>,
Expand Down Expand Up @@ -1046,12 +1101,6 @@ fn get_filter_logs(_params: Params, _node: &Arc<Mutex<Node>>) -> 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<Mutex<Node>>) -> 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<Mutex<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 @@ -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<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 @@ -30,6 +30,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 @@ -382,6 +395,43 @@ impl State {
&bincode::serialize(&account)?,
)?)
}

pub fn get_proof(&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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make this &mut self instead? You already make a clone of it in the API implementation (by calling .at_root).

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DrZoltanFazekas Are we happy that this exposes our trie encoding as part of our external API (and thus makes it hard to ever change it)?

I ask because I don't think we are encoding things into the trie the same way as Ethereum does and thus there's no guarantee that what we're doing is sensible.

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 {
Expand Down
115 changes: 113 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::{hex, Address};
use alloy::primitives::{hex, Address, B256};
use eth_trie::{EthTrie, MemoryDB, Trie};
use ethabi::{ethereum_types::U64, Token};
use ethers::{
abi::FunctionExt,
Expand All @@ -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};

Expand Down Expand Up @@ -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::<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 = bincode::deserialize::<Account>(&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::<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());
}
}
Loading