Skip to content

Commit

Permalink
Merge pull request #102 from Analog-Labs/eth-storage-proof
Browse files Browse the repository at this point in the history
Eth storage proof
  • Loading branch information
dvc94ch authored Mar 2, 2023
2 parents 7f8e297 + 340b477 commit 207ee31
Show file tree
Hide file tree
Showing 12 changed files with 469 additions and 124 deletions.
117 changes: 88 additions & 29 deletions chains/ethereum/server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::eth_types::GENESIS_BLOCK_INDEX;
use crate::utils::{get_block, get_transaction, populate_transactions, EthDetokenizer};
use anyhow::{anyhow, bail, Context, Result};
use ethers::abi::Abi;
use ethers::contract as ethers_contract;
use crate::utils::{
get_block, get_transaction, parse_method, populate_transactions, EthDetokenizer,
};
use anyhow::{bail, Context, Result};
use ethers::prelude::*;
use ethers::utils::keccak256;
use ethers::utils::rlp::Encodable;
use proof::verify_proof;
use rosetta_config_ethereum::{EthereumMetadata, EthereumMetadataParams};
use rosetta_server::crypto::address::Address;
use rosetta_server::crypto::PublicKey;
Expand All @@ -12,11 +15,12 @@ use rosetta_server::types::{
TransactionIdentifier,
};
use rosetta_server::{BlockchainClient, BlockchainConfig};
use serde_json::Value;
use serde_json::{json, Value};
use std::str::FromStr;
use std::sync::Arc;

mod eth_types;
mod proof;
mod utils;

pub struct EthereumClient {
Expand All @@ -33,7 +37,10 @@ impl BlockchainClient for EthereumClient {
async fn new(network: &str, addr: &str) -> Result<Self> {
let config = rosetta_config_ethereum::config(network)?;
let client = Arc::new(Provider::<Http>::try_from(format!("http://{addr}"))?);
let genesis = client.get_block(0).await?.unwrap();
let genesis = client
.get_block(0)
.await?
.context("Failed to get genesis block")?;
let genesis_block = BlockIdentifier {
index: 0,
hash: hex::encode(genesis.hash.as_ref().unwrap()),
Expand Down Expand Up @@ -113,8 +120,7 @@ impl BlockchainClient for EthereumClient {
let from: H160 = public_key
.to_address(self.config().address_format)
.address()
.parse()
.unwrap();
.parse()?;
let to = H160::from_slice(&options.destination);
let chain_id = self.client.get_chainid().await?;
let nonce = self.client.get_transaction_count(from, None).await?;
Expand All @@ -137,12 +143,13 @@ impl BlockchainClient for EthereumClient {

async fn submit(&self, transaction: &[u8]) -> Result<Vec<u8>> {
let tx = transaction.to_vec().into();

Ok(self
.client
.send_raw_transaction(Bytes(tx))
.await?
.await?
.unwrap()
.context("Failed to get transaction receipt")?
.transaction_hash
.0
.to_vec())
Expand Down Expand Up @@ -202,42 +209,41 @@ impl BlockchainClient for EthereumClient {
match call_type.to_lowercase().as_str() {
"call" => {
//process constant call
let abi_str = params["abi"].as_str().context("ABI not found")?;

let abi: Abi = serde_json::from_str(abi_str).map_err(|err| anyhow!(err))?;

let contract_address = H160::from_str(
params["contract_address"]
.as_str()
.context("contact address not found")?,
)
.map_err(|err| anyhow!(err))?;
)?;

let function = parse_method(&method)?;

let bytes: Vec<u8> = function.encode_input(&[])?;

let tx = Eip1559TransactionRequest {
to: Some(contract_address.into()),
data: Some(bytes.into()),
..Default::default()
};

let tx = &tx.into();
let received_data = self.client.call(tx, None).await?;

let contract =
ethers_contract::Contract::new(contract_address, abi, self.client.clone());
let data: EthDetokenizer = decode_function_data(&function, received_data, false)?;

let value: EthDetokenizer = contract
.method(&method, ())
.map_err(|err| anyhow!(err))?
.call()
.await
.map_err(|err| anyhow!(err))?;
let result: Value = serde_json::from_str(&data.json)?;

let result: Value = serde_json::from_str(&value.json)?;
return Ok(result);
}
"storage" => {
//process storage call
let from = H160::from_str(
params["address"]
params["contract_address"]
.as_str()
.context("address field not found")?,
)
.map_err(|err| anyhow!(err))?;
)?;

let location =
H256::from_str(params["position"].as_str().context("position not found")?)
.map_err(|err| anyhow!(err))?;
H256::from_str(params["position"].as_str().context("position not found")?)?;

let block_num = params["block_number"]
.as_u64()
Expand All @@ -249,6 +255,47 @@ impl BlockchainClient for EthereumClient {
.await?;
return Ok(Value::String(format!("{storage_check:#?}",)));
}
"storage_proof" => {
let from = H160::from_str(
params["contract_address"]
.as_str()
.context("address field not found")?,
)?;

let location =
H256::from_str(params["position"].as_str().context("position not found")?)?;

let block_num = params["block_number"]
.as_u64()
.map(|block_num| BlockId::Number(block_num.into()));

let proof_data = self
.client
.get_proof(from, vec![location], block_num)
.await?;

//process verfiicatin of proof
let storage_hash = proof_data.storage_hash;
let storage_proof = proof_data.storage_proof.first().context("No proof found")?;

let key = &storage_proof.key;
let key_hash = keccak256(key);
let encoded_val = storage_proof.value.rlp_bytes().to_vec();

let is_valid = verify_proof(
&storage_proof.proof,
storage_hash.as_bytes(),
&key_hash.to_vec(),
&encoded_val,
);

let result = serde_json::to_value(&proof_data)?;

return Ok(json!({
"proof": result,
"isValid": is_valid
}));
}
_ => {
bail!("request type not supported")
}
Expand Down Expand Up @@ -289,4 +336,16 @@ mod tests {
let config = rosetta_config_ethereum::config("dev")?;
rosetta_server::tests::construction(config).await
}

#[tokio::test]
async fn test_find_transaction() -> Result<()> {
let config = rosetta_config_ethereum::config("dev")?;
rosetta_server::tests::find_transaction(config).await
}

#[tokio::test]
async fn test_list_transactions() -> Result<()> {
let config = rosetta_config_ethereum::config("dev")?;
rosetta_server::tests::list_transactions(config).await
}
}
194 changes: 194 additions & 0 deletions chains/ethereum/server/src/proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
use ethers::types::{Bytes, EIP1186ProofResponse};
use ethers::utils::keccak256;
use ethers::utils::rlp::{decode_list, RlpStream};

pub fn verify_proof(proof: &Vec<Bytes>, root: &[u8], path: &Vec<u8>, value: &Vec<u8>) -> bool {
let mut expected_hash = root.to_vec();
let mut path_offset = 0;

for (i, node) in proof.iter().enumerate() {
if expected_hash != keccak256(node).to_vec() {
return false;
}

let node_list: Vec<Vec<u8>> = decode_list(node);

if node_list.len() == 17 {
if i == proof.len() - 1 {
// exclusion proof
let nibble = get_nibble(path, path_offset);
let node = &node_list[nibble as usize];

if node.is_empty() && is_empty_value(value) {
return true;
}
} else {
let nibble = get_nibble(path, path_offset);
expected_hash = node_list[nibble as usize].clone();

path_offset += 1;
}
} else if node_list.len() == 2 {
if i == proof.len() - 1 {
// exclusion proof
if !paths_match(&node_list[0], skip_length(&node_list[0]), path, path_offset)
&& is_empty_value(value)
{
return true;
}

// inclusion proof
if &node_list[1] == value {
return paths_match(
&node_list[0],
skip_length(&node_list[0]),
path,
path_offset,
);
}
} else {
let node_path = &node_list[0];
let prefix_length = shared_prefix_length(path, path_offset, node_path);
if prefix_length < node_path.len() * 2 - skip_length(node_path) {
// The proof shows a divergent path, but we're not
// at the end of the proof, so something's wrong.
return false;
}
path_offset += prefix_length;
expected_hash = node_list[1].clone();
}
} else {
return false;
}
}

false
}

fn paths_match(p1: &Vec<u8>, s1: usize, p2: &Vec<u8>, s2: usize) -> bool {
let len1 = p1.len() * 2 - s1;
let len2 = p2.len() * 2 - s2;

if len1 != len2 {
return false;
}

for offset in 0..len1 {
let n1 = get_nibble(p1, s1 + offset);
let n2 = get_nibble(p2, s2 + offset);

if n1 != n2 {
return false;
}
}

true
}

#[allow(dead_code)]
fn get_rest_path(p: &Vec<u8>, s: usize) -> String {
let mut ret = String::new();
for i in s..p.len() * 2 {
let n = get_nibble(p, i);
ret += &format!("{n:01x}");
}
ret
}

fn is_empty_value(value: &Vec<u8>) -> bool {
let mut stream = RlpStream::new();
stream.begin_list(4);
stream.append_empty_data();
stream.append_empty_data();
let empty_storage_hash = "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421";
stream.append(&hex::decode(empty_storage_hash).unwrap());
let empty_code_hash = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";
stream.append(&hex::decode(empty_code_hash).unwrap());
let empty_account = stream.out();

let is_empty_slot = value.len() == 1 && value[0] == 0x80;
let is_empty_account = value == &empty_account;
is_empty_slot || is_empty_account
}

fn shared_prefix_length(path: &Vec<u8>, path_offset: usize, node_path: &Vec<u8>) -> usize {
let skip_length = skip_length(node_path);

let len = std::cmp::min(
node_path.len() * 2 - skip_length,
path.len() * 2 - path_offset,
);
let mut prefix_len = 0;

for i in 0..len {
let path_nibble = get_nibble(path, i + path_offset);
let node_path_nibble = get_nibble(node_path, i + skip_length);

if path_nibble == node_path_nibble {
prefix_len += 1;
} else {
break;
}
}

prefix_len
}

fn skip_length(node: &Vec<u8>) -> usize {
if node.is_empty() {
return 0;
}

let nibble = get_nibble(node, 0);
match nibble {
0 => 2,
1 => 1,
2 => 2,
3 => 1,
_ => 0,
}
}

fn get_nibble(path: &[u8], offset: usize) -> u8 {
let byte = path[offset / 2];
if offset % 2 == 0 {
byte >> 4
} else {
byte & 0xF
}
}

pub fn _encode_account(proof: &EIP1186ProofResponse) -> Vec<u8> {
let mut stream = RlpStream::new_list(4);
stream.append(&proof.nonce);
stream.append(&proof.balance);
stream.append(&proof.storage_hash);
stream.append(&proof.code_hash);
let encoded = stream.out();
encoded.to_vec()
}

#[cfg(test)]
mod tests {
use crate::proof::shared_prefix_length;

#[tokio::test]
async fn test_shared_prefix_length() {
// We compare the path starting from the 6th nibble i.e. the 6 in 0x6f
let path: Vec<u8> = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21];
let path_offset = 6;
// Our node path matches only the first 5 nibbles of the path
let node_path: Vec<u8> = vec![0x6f, 0x6c, 0x63, 0x21];
let shared_len = shared_prefix_length(&path, path_offset, &node_path);
assert_eq!(shared_len, 5);

// Now we compare the path starting from the 5th nibble i.e. the 4 in 0x14
let path: Vec<u8> = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21];
let path_offset = 5;
// Our node path matches only the first 7 nibbles of the path
// Note the first nibble is 1, so we skip 1 nibble
let node_path: Vec<u8> = vec![0x14, 0x6f, 0x6c, 0x64, 0x11];
let shared_len = shared_prefix_length(&path, path_offset, &node_path);
assert_eq!(shared_len, 7);
}
}
Loading

0 comments on commit 207ee31

Please sign in to comment.