Skip to content

Commit

Permalink
Add GethClient and support for minimum required JSON-RPC endpoints by…
Browse files Browse the repository at this point in the history
… bus-mapping. (privacy-scaling-explorations#171)

* Add ProviderError

* Add rpc module

* Add ethrs-providers to deps and url & tokio to dev-deps

* Add `GethQueries` and getBlockByHash fn

* Change `GethQueries` by `GethClient`

* Add `eth_getBlockByNumber` support

* Update `serde` version to `1.0.130`.

* Impl `Serialize` for `GethExecStep` & `GethExecStepInternal`

Since the `json_rpc` lib that we use requires the response types to be:
`Serialize + Deserialize`, it has been needed to impl `Serialize` for
the types that were lacking it.

- GethExecStepInternal now derives `Serialize`.
- Implemented `From<GethExecStep> for GethExecStepInternal` which is
  used in the next point.
- Implement `Serialize` for `GethExecStep` serializing it as a
  `GethExecStepInternal` using the previous `From` impl mentioned.

* Add `ResultGethExecTrace` & `ResultGethExecStep`

Geth return value for `debug_traceBlockByHash` and
`debug_traceBlockByNumber` isn't the expected.

While the expected format was to get from these calls
`Vec<GethExecTrace>`, you are given a similar vector but that contains a
`result` Json field that makes no sense previous to each trace.

Therefore it has been needed to simulate this as a new type and derive
`Serialize` and `Deserialize` for it so that the response from this call
can be parsed from the JSON and turned into usable data structures.

* Add support for debug_traceBlockByHash/Number

Following @ed255 suggestions the following extra JSON-RPC methods have
been provided support:
- debug_traceBlockByHash
- debug_traceBlockByNumber

Resolves: privacy-scaling-explorations#23

* Fix clippy complaints

* Fix review suggestions from @ed255

- Renames ResultGethExecTraces and ResultGethExecTrace.
- Fix `From<GethExecStep` impl for `GethExecStepInternal`.

Co-authored-by: Eduard S. <[email protected]>
  • Loading branch information
CPerezz and ed255 authored Nov 17, 2021
1 parent 1569b8d commit ef6a070
Showing 5 changed files with 257 additions and 3 deletions.
5 changes: 4 additions & 1 deletion bus-mapping/Cargo.toml
Original file line number Diff line number Diff line change
@@ -9,13 +9,16 @@ authors = ["CPerezz <[email protected]>"]
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.
@@ -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 {
60 changes: 58 additions & 2 deletions bus-mapping/src/eth_types.rs
Original file line number Diff line number Diff line change
@@ -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,
@@ -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
@@ -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)]
#[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,
1 change: 1 addition & 0 deletions bus-mapping/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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())
}
}

#[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());
}
}

0 comments on commit ef6a070

Please sign in to comment.