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

Eth storage proof #102

Merged
merged 11 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
114 changes: 86 additions & 28 deletions chains/ethereum/server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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 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,10 +13,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 utils::{hex_str_to_bytes, parse_method};

mod eth_types;
mod proof;
mod utils;

pub struct EthereumClient {
Expand All @@ -32,7 +35,10 @@ impl BlockchainClient for EthereumClient {
async fn new(network: &str, addr: &str) -> Result<Self> {
let config = rosetta_config_ethereum::config(network)?;
let client = 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 @@ -112,8 +118,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 @@ -136,12 +141,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 @@ -201,42 +207,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 contract =
ethers_contract::Contract::new(contract_address, abi, self.client.clone());
let function = parse_method(&method)?;

let value: EthDetokenizer = contract
.method(&method, ())
.map_err(|err| anyhow!(err))?
.call()
.await
.map_err(|err| anyhow!(err))?;
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 data: EthDetokenizer = decode_function_data(&function, received_data, false)?;

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 @@ -248,6 +253,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 = hex_str_to_bytes(&hex::encode(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 @@ -288,4 +334,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