Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Add GethClient and support for minimum required JSON-RPC endpoints by bus-mapping. #171

Merged
merged 12 commits into from
Nov 17, 2021
5 changes: 4 additions & 1 deletion bus-mapping/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ authors = ["CPerezz <c.perezbaro@gmail.com>"]
ff = "0.11"
pasta_curves = "0.1"
itertools = "0.10"
serde = {version = "1.0.127", features = ["derive"] }
serde = {version = "1.0.130", features = ["derive"] }
lazy_static = "1.4"
serde_json = "1.0.66"
hex = "0.4"
geth-utils = { path = "../geth-utils" }
web3 = {version = "0.17", default-features = false}
uint = "0.9.1"
ethers-providers = "0.5.5"

[dev-dependencies]
url = "2.2.2"
tokio = { version = "1.13", features = ["macros"] }
pretty_assertions = "1.0.0"
9 changes: 9 additions & 0 deletions bus-mapping/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Error module for the bus-mapping crate

use core::fmt::{Display, Formatter, Result as FmtResult};
use ethers_providers::ProviderError;
use std::error::Error as StdError;

/// Error type for any BusMapping related failure.
Expand All @@ -26,6 +27,14 @@ pub enum Error {
WordToMemAddr,
/// Error while generating a trace.
TracingError,
/// JSON-RPC related error
JSONRpcError(ProviderError),
}

impl From<ProviderError> for Error {
fn from(err: ProviderError) -> Self {
Error::JSONRpcError(err)
}
}

impl Display for Error {
Expand Down
60 changes: 58 additions & 2 deletions bus-mapping/src/eth_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl<F: FieldExt> ToScalar<F> for Address {
}
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[doc(hidden)]
struct GethExecStepInternal {
pc: ProgramCounter,
Expand Down Expand Up @@ -171,6 +171,48 @@ pub struct GethExecStep {
pub storage: Storage,
}

impl From<GethExecStep> for GethExecStepInternal {
fn from(step: GethExecStep) -> Self {
GethExecStepInternal {
pc: step.pc,
op: step.op,
gas: step.gas,
gas_cost: step.gas_cost,
depth: step.depth,
stack: step
.stack
.0
.iter()
.map(|stack_elem| DebugU256(stack_elem.0))
.collect(),
memory: step
.memory
.0
.chunks(32)
.map(|word| DebugU256::from_big_endian(word))
.collect(),
storage: step
.storage
.0
.iter()
.map(|(k, v)| (DebugU256(k.0), DebugU256(v.0)))
.collect(),
}
}
}

// TODO: Tried `#[serde(into = "IntoType")]` feature but doesn't seem to work. Double check.
impl Serialize for GethExecStep {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// Serialize as a `GethExecStepInternal`
let internal = GethExecStepInternal::from(self.clone());
internal.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for GethExecStep {
fn deserialize<D>(deserializer: D) -> Result<GethExecStep, D::Error>
where
Expand Down Expand Up @@ -202,9 +244,23 @@ impl<'de> Deserialize<'de> for GethExecStep {
}
}

/// Helper type built to deal with the weird `result` field added between `GethExecutionTrace`s in
/// `debug_traceBlockByHash` and `debug_traceBlockByNumber` Geth JSON-RPC calls.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
#[doc(hidden)]
pub(crate) struct ResultGethExecTraces(pub(crate) Vec<ResultGethExecTrace>);

/// Helper type built to deal with the weird `result` field added between `GethExecutionTrace`s in
/// `debug_traceBlockByHash` and `debug_traceBlockByNumber` Geth JSON-RPC calls.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[doc(hidden)]
pub(crate) struct ResultGethExecTrace {
pub(crate) result: GethExecTrace,
}

/// The execution trace type returned by geth RPC debug_trace* methods. Corresponds to
/// `ExecutionResult` in `go-ethereum/internal/ethapi/api.go`.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[doc(hidden)]
pub struct GethExecTrace {
pub gas: Gas,
Expand Down
1 change: 1 addition & 0 deletions bus-mapping/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,5 +222,6 @@ pub mod circuit_input_builder;
#[macro_use]
pub mod eth_types;
pub mod mock;
pub mod rpc;
pub use error::Error;
pub use exec_trace::BlockConstants;
185 changes: 185 additions & 0 deletions bus-mapping/src/rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//! Module which contains all the RPC calls that are needed at any point to query a Geth node in order to get a Block, Tx or Trace info.

use crate::eth_types::{
Block, GethExecTrace, Hash, ResultGethExecTraces, Transaction, U64,
};
use crate::Error;
use ethers_providers::JsonRpcClient;

/// Serialize a type.
///
/// # Panics
///
/// If the type returns an error during serialization.
pub fn serialize<T: serde::Serialize>(t: &T) -> serde_json::Value {
serde_json::to_value(t).expect("Types never fail to serialize.")
}

/// Struct used to define the input that you want to provide to the `eth_getBlockByNumber` call as it mixes numbers with string literals.
#[derive(Debug)]
pub enum BlockNumber {
/// Specific block number
Num(U64),
/// Earliest block
Earliest,
/// Latest block
Latest,
/// Pending block
Pending,
}

impl From<u64> for BlockNumber {
fn from(num: u64) -> Self {
BlockNumber::Num(U64::from(num))
}
}

impl BlockNumber {
/// Serializes a BlockNumber as a [`Value`](serde_json::Value) to be able to throw it into a JSON-RPC request.
pub fn serialize(self) -> serde_json::Value {
match self {
BlockNumber::Num(num) => serialize(&num),
BlockNumber::Earliest => serialize(&"earliest"),
BlockNumber::Latest => serialize(&"latest"),
BlockNumber::Pending => serialize(&"pending"),
}
}
}

/// Placeholder structure designed to contain the methods that the BusMapping needs in order to enable Geth queries.
pub struct GethClient<P: JsonRpcClient>(P);

impl<P: JsonRpcClient> GethClient<P> {
/// Generates a new `GethClient` instance.
pub fn new(provider: P) -> Self {
Self(provider)
}

/// Calls `eth_getBlockByHash` via JSON-RPC returning a [`Block`] returning all the block information including it's transaction's details.
pub async fn get_block_by_hash(
&self,
hash: Hash,
) -> Result<Block<Transaction>, Error> {
let hash = serialize(&hash);
let flag = serialize(&true);
self.0
.request("eth_getBlockByHash", [hash, flag])
.await
.map_err(|e| Error::JSONRpcError(e.into()))
}

/// Calls `eth_getBlockByNumber` via JSON-RPC returning a [`Block`] returning all the block information including it's transaction's details.
pub async fn get_block_by_number(
&self,
block_num: BlockNumber,
) -> Result<Block<Transaction>, Error> {
let num = block_num.serialize();
let flag = serialize(&true);
self.0
.request("eth_getBlockByNumber", [num, flag])
.await
.map_err(|e| Error::JSONRpcError(e.into()))
}

/// Calls `debug_traceBlockByHash` via JSON-RPC returning a [`Vec<GethExecTrace>`] with each GethTrace corresponding to 1 transaction of the block.
pub async fn trace_block_by_hash(
&self,
hash: Hash,
) -> Result<Vec<GethExecTrace>, Error> {
let hash = serialize(&hash);
let resp: ResultGethExecTraces = self
.0
.request("debug_traceBlockByHash", [hash])
.await
.map_err(|e| Error::JSONRpcError(e.into()))?;
Ok(resp.0.into_iter().map(|step| step.result).collect())
}

/// Calls `debug_traceBlockByNumber` via JSON-RPC returning a [`Vec<GethExecTrace>`] with each GethTrace corresponding to 1 transaction of the block.
pub async fn trace_block_by_number(
&self,
block_num: BlockNumber,
) -> Result<Vec<GethExecTrace>, Error> {
let num = block_num.serialize();
let resp: ResultGethExecTraces = self
.0
.request("debug_traceBlockByNumber", [num])
.await
.map_err(|e| Error::JSONRpcError(e.into()))?;
Ok(resp.0.into_iter().map(|step| step.result).collect())
}
Comment on lines +84 to +110
Copy link

Choose a reason for hiding this comment

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

FWIW we'd be happy to upstream the Geth Tracing logic to ethers-rs, similar to the parity tracing module https://github.com/gakonst/ethers-rs/blob/master/ethers-providers/src/provider.rs#L788

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem is that we use a slightly different variant of the types that you have defined in your lib.

That's why we did not make a PR adding them in ethers and instead just took the Provider backend and used it directly.

Will try to add this in a PR in ethers once I get a bit more time :)

}

#[cfg(test)]
mod rpc_tests {
use super::*;
use ethers_providers::Http;
use std::str::FromStr;
use url::Url;

// The test is ignored as the values used depend on the Geth instance used each time you run the tests.
// And we can't assume that everyone will have a Geth client synced with mainnet to have unified "test-vectors".
#[ignore]
#[tokio::test]
async fn test_get_block_by_hash() {
let transport = Http::new(Url::parse("http://localhost:8545").unwrap());

let hash = Hash::from_str("0xe4f7aa19a76fcf31a6adff3b400300849e39dd84076765fb3af09d05ee9d787a").unwrap();
let prov = GethClient::new(transport);
let block_by_hash = prov.get_block_by_hash(hash).await.unwrap();
assert!(hash == block_by_hash.hash.unwrap());
}

// The test is ignored as the values used depend on the Geth instance used each time you run the tests.
// And we can't assume that everyone will have a Geth client synced with mainnet to have unified "test-vectors".
#[ignore]
#[tokio::test]
async fn test_get_block_by_number() {
let transport = Http::new(Url::parse("http://localhost:8545").unwrap());

let hash = Hash::from_str("0xe4f7aa19a76fcf31a6adff3b400300849e39dd84076765fb3af09d05ee9d787a").unwrap();
let prov = GethClient::new(transport);
let block_by_num_latest =
prov.get_block_by_number(BlockNumber::Latest).await.unwrap();
assert!(hash == block_by_num_latest.hash.unwrap());
let block_by_num = prov.get_block_by_number(1u64.into()).await.unwrap();
assert!(
block_by_num.transactions[0].hash
== block_by_num_latest.transactions[0].hash
);
}

// The test is ignored as the values used depend on the Geth instance used each time you run the tests.
// And we can't assume that everyone will have a Geth client synced with mainnet to have unified "test-vectors".
#[ignore]
#[tokio::test]
async fn test_trace_block_by_hash() {
let transport = Http::new(Url::parse("http://localhost:8545").unwrap());

let hash = Hash::from_str("0xe2d191e9f663a3a950519eadeadbd614965b694a65a318a0b8f053f2d14261ff").unwrap();
let prov = GethClient::new(transport);
let trace_by_hash = prov.trace_block_by_hash(hash).await.unwrap();
// Since we called in the test block the same transaction twice the len should be the same and != 0.
assert!(
trace_by_hash[0].struct_logs.len()
== trace_by_hash[1].struct_logs.len()
);
assert!(!trace_by_hash[0].struct_logs.is_empty());
}

// The test is ignored as the values used depend on the Geth instance used each time you run the tests.
// And we can't assume that everyone will have a Geth client synced with mainnet to have unified "test-vectors".
#[ignore]
#[tokio::test]
async fn test_trace_block_by_number() {
let transport = Http::new(Url::parse("http://localhost:8545").unwrap());
let prov = GethClient::new(transport);
let trace_by_hash = prov.trace_block_by_number(5.into()).await.unwrap();
// Since we called in the test block the same transaction twice the len should be the same and != 0.
assert!(
trace_by_hash[0].struct_logs.len()
== trace_by_hash[1].struct_logs.len()
);
assert!(!trace_by_hash[0].struct_logs.is_empty());
}
}