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

feat: add support for Geth built-in tracer and config #2121

Merged
merged 12 commits into from
Feb 7, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Add abigen support for hardhat generated bytecode json format [#2012](https://github.com/gakonst/ethers-rs/pull/2012)
- Fix typo in `RwClient` docs for `write_client` method.
- Add support for Geth `debug_traceCall` [#1949](https://github.com/gakonst/ethers-rs/pull/1949)
- Add support for Geth built-in tracer and config [#2121](https://github.com/gakonst/ethers-rs/pull/2121)
- Graceful handling of WebSocket transport errors [#1889](https://github.com/gakonst/ethers-rs/issues/1889) [#1815](https://github.com/gakonst/ethers-rs/issues/1815)
- `MiddlewareBuilder` trait to instantiate a `Provider` as `Middleware` layers.
- An `Event` builder can be instantiated specifying the event filter type, without the need to instantiate a contract.
Expand Down
84 changes: 60 additions & 24 deletions ethers-core/src/types/trace/geth.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
mod call;
mod four_byte;
mod noop;
mod pre_state;

pub use self::{
call::{CallConfig, CallFrame},
four_byte::FourByteFrame,
noop::NoopFrame,
pre_state::{PreStateConfig, PreStateFrame},
};
use crate::{
types::{Address, Bytes, NameOrAddress, H256, U256},
types::{Bytes, H256, U256},
utils::from_int_or_hex,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -40,34 +51,14 @@ pub struct StructLog {
pub storage: Option<BTreeMap<H256, H256>>,
}

// https://github.com/ethereum/go-ethereum/blob/a9ef135e2dd53682d106c6a2aede9187026cc1de/eth/tracers/native/call.go#L37
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallFrame {
#[serde(rename = "type")]
pub typ: String,
pub from: Address,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to: Option<NameOrAddress>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<U256>,
#[serde(deserialize_with = "from_int_or_hex")]
pub gas: U256,
#[serde(deserialize_with = "from_int_or_hex", rename = "gasUsed")]
pub gas_used: U256,
pub input: Bytes,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<Bytes>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calls: Option<Vec<CallFrame>>,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum GethTraceFrame {
Default(DefaultFrame),
NoopTracer(NoopFrame),
FourByteTracer(FourByteFrame),
CallTracer(CallFrame),
PreStateTracer(PreStateFrame),
}

impl From<DefaultFrame> for GethTraceFrame {
Expand All @@ -76,12 +67,30 @@ impl From<DefaultFrame> for GethTraceFrame {
}
}

impl From<FourByteFrame> for GethTraceFrame {
fn from(value: FourByteFrame) -> Self {
GethTraceFrame::FourByteTracer(value)
}
}

impl From<CallFrame> for GethTraceFrame {
fn from(value: CallFrame) -> Self {
GethTraceFrame::CallTracer(value)
}
}

impl From<PreStateFrame> for GethTraceFrame {
fn from(value: PreStateFrame) -> Self {
GethTraceFrame::PreStateTracer(value)
}
}

impl From<NoopFrame> for GethTraceFrame {
fn from(value: NoopFrame) -> Self {
GethTraceFrame::NoopTracer(value)
}
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum GethTrace {
Expand All @@ -106,8 +115,21 @@ impl From<Value> for GethTrace {
/// See <https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers>
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub enum GethDebugBuiltInTracerType {
#[serde(rename = "4byteTracer")]
FourByteTracer,
#[serde(rename = "callTracer")]
CallTracer,
#[serde(rename = "prestateTracer")]
PreStateTracer,
#[serde(rename = "noopTracer")]
NoopTracer,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum GethDebugBuiltInTracerConfig {
CallTracer(CallConfig),
PreStateTracer(PreStateConfig),
}

/// Available tracers
Expand All @@ -123,6 +145,16 @@ pub enum GethDebugTracerType {
JsTracer(String),
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum GethDebugTracerConfig {
/// built-in tracer
BuiltInTracer(GethDebugBuiltInTracerConfig),

/// custom JS tracer
JsTracer(Value),
}

/// Bindings for additional `debug_traceTransaction` options
///
/// See <https://geth.ethereum.org/docs/rpc/ns-debug#debug_tracetransaction>
Expand All @@ -139,6 +171,10 @@ pub struct GethDebugTracingOptions {
pub enable_return_data: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tracer: Option<GethDebugTracerType>,
/// tracerConfig is slated for Geth v1.11.0
/// See <https://github.com/ethereum/go-ethereum/issues/26513>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tracer_config: Option<GethDebugTracerConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<String>,
}
Expand Down
87 changes: 87 additions & 0 deletions ethers-core/src/types/trace/geth/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::{
types::{Address, Bytes, NameOrAddress, H256, U256},
utils::from_int_or_hex,
};
use serde::{Deserialize, Serialize};

// https://github.com/ethereum/go-ethereum/blob/91cb6f863a965481e51d5d9c0e5ccd54796fd967/eth/tracers/native/call.go#L44
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallFrame {
#[serde(rename = "type")]
pub typ: String,
pub from: Address,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to: Option<NameOrAddress>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<U256>,
#[serde(default, deserialize_with = "from_int_or_hex")]
pub gas: U256,
#[serde(default, deserialize_with = "from_int_or_hex", rename = "gasUsed")]
pub gas_used: U256,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The gas and gas_used fields are not compatible with the legacy format. Should we change it to Option, and which might be a break change?
For now, to be compatible with previous versions, I've only added a default handling, which returns U256::zero() instead of None.

Copy link
Owner

Choose a reason for hiding this comment

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

default is fine w me

pub input: Bytes,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<Bytes>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calls: Option<Vec<CallFrame>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logs: Option<Vec<CallLogFrame>>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallLogFrame {
#[serde(default, skip_serializing_if = "Option::is_none")]
address: Option<Address>,
#[serde(default, skip_serializing_if = "Option::is_none")]
topics: Option<Vec<H256>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
data: Option<Bytes>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub only_top_call: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub with_log: Option<bool>,
}

#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;

// See <https://github.com/ethereum/go-ethereum/tree/master/eth/tracers/internal/tracetest/testdata>
const DEFAULT: &str = include_str!("./test_data/call_tracer/default.json");
const LEGACY: &str = include_str!("./test_data/call_tracer/legacy.json");
const ONLY_TOP_CALL: &str = include_str!("./test_data/call_tracer/only_top_call.json");
const WITH_LOG: &str = include_str!("./test_data/call_tracer/with_log.json");

#[test]
fn test_serialize_call_trace() {
let mut opts = GethDebugTracingCallOptions::default();
opts.tracing_options.disable_storage = Some(false);
opts.tracing_options.tracer =
Some(GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer));
opts.tracing_options.tracer_config =
Some(GethDebugTracerConfig::BuiltInTracer(GethDebugBuiltInTracerConfig::CallTracer(
CallConfig { only_top_call: Some(true), with_log: Some(true) },
)));

assert_eq!(
serde_json::to_string(&opts).unwrap(),
r#"{"disableStorage":false,"tracer":"callTracer","tracerConfig":{"onlyTopCall":true,"withLog":true}}"#
);
}

#[test]
fn test_deserialize_call_trace() {
let _trace: CallFrame = serde_json::from_str(DEFAULT).unwrap();
let _trace: CallFrame = serde_json::from_str(LEGACY).unwrap();
let _trace: CallFrame = serde_json::from_str(ONLY_TOP_CALL).unwrap();
let trace: CallFrame = serde_json::from_str(WITH_LOG).unwrap();
let _logs = trace.logs.unwrap();
}
}
34 changes: 34 additions & 0 deletions ethers-core/src/types/trace/geth/four_byte.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

// https://github.com/ethereum/go-ethereum/blob/91cb6f863a965481e51d5d9c0e5ccd54796fd967/eth/tracers/native/4byte.go#L48
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FourByteFrame(pub BTreeMap<String, u64>);

#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;

const DEFAULT: &str = r#"{
"0x27dc297e-128": 1,
"0x38cc4831-0": 2,
"0x524f3889-96": 1,
"0xadf59f99-288": 1,
"0xc281d19e-0": 1
}"#;

#[test]
fn test_serialize_four_byte_trace() {
let mut opts = GethDebugTracingCallOptions::default();
opts.tracing_options.tracer =
Some(GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::FourByteTracer));

assert_eq!(serde_json::to_string(&opts).unwrap(), r#"{"tracer":"4byteTracer"}"#);
}

#[test]
fn test_deserialize_four_byte_trace() {
let _trace: FourByteFrame = serde_json::from_str(DEFAULT).unwrap();
}
}
30 changes: 30 additions & 0 deletions ethers-core/src/types/trace/geth/noop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

// https://github.com/ethereum/go-ethereum/blob/91cb6f863a965481e51d5d9c0e5ccd54796fd967/eth/tracers/native/noop.go#L34
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoopFrame(BTreeMap<Null, Null>);
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
struct Null;

#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;

const DEFAULT: &str = r#"{}"#;

#[test]
fn test_serialize_noop_trace() {
let mut opts = GethDebugTracingCallOptions::default();
opts.tracing_options.tracer =
Some(GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::NoopTracer));

assert_eq!(serde_json::to_string(&opts).unwrap(), r#"{"tracer":"noopTracer"}"#);
}

#[test]
fn test_deserialize_noop_trace() {
let _trace: NoopFrame = serde_json::from_str(DEFAULT).unwrap();
}
}
92 changes: 92 additions & 0 deletions ethers-core/src/types/trace/geth/pre_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use crate::{
types::{Address, H256, U256},
utils::from_int_or_hex_opt,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

// https://github.com/ethereum/go-ethereum/blob/91cb6f863a965481e51d5d9c0e5ccd54796fd967/eth/tracers/native/prestate.go#L38
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PreStateFrame {
Default(PreStateMode),
Diff(DiffMode),
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PreStateMode(pub BTreeMap<Address, AccountState>);
Comment on lines +16 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this should probably b #[serde(transparent)]?

https://serde.rs/container-attrs.html#transparent

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems transparent is enabled for newtype by default?

Copy link
Collaborator

Choose a reason for hiding this comment

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

TIL


#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiffMode {
pub pre: BTreeMap<Address, AccountState>,
pub post: BTreeMap<Address, AccountState>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccountState {
#[serde(
default,
deserialize_with = "from_int_or_hex_opt",
skip_serializing_if = "Option::is_none"
)]
pub balance: Option<U256>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(
default,
deserialize_with = "from_int_or_hex_opt",
skip_serializing_if = "Option::is_none"
)]
pub nonce: Option<U256>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage: Option<BTreeMap<H256, H256>>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PreStateConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diff_mode: Option<bool>,
}

#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;

// See <https://github.com/ethereum/go-ethereum/tree/master/eth/tracers/internal/tracetest/testdata>
const DEFAULT: &str = include_str!("./test_data/pre_state_tracer/default.json");
const LEGACY: &str = include_str!("./test_data/pre_state_tracer/legacy.json");
const DIFF_MODE: &str = include_str!("./test_data/pre_state_tracer/diff_mode.json");

#[test]
fn test_serialize_pre_state_trace() {
let mut opts = GethDebugTracingCallOptions::default();
opts.tracing_options.disable_storage = Some(false);
opts.tracing_options.tracer =
Some(GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::PreStateTracer));
opts.tracing_options.tracer_config = Some(GethDebugTracerConfig::BuiltInTracer(
GethDebugBuiltInTracerConfig::PreStateTracer(PreStateConfig { diff_mode: Some(true) }),
));

assert_eq!(
serde_json::to_string(&opts).unwrap(),
r#"{"disableStorage":false,"tracer":"prestateTracer","tracerConfig":{"diffMode":true}}"#
);
}

#[test]
fn test_deserialize_pre_state_trace() {
let trace: PreStateFrame = serde_json::from_str(DEFAULT).unwrap();
match trace {
PreStateFrame::Default(PreStateMode(_)) => {}
_ => unreachable!(),
}
let _trace: PreStateFrame = serde_json::from_str(LEGACY).unwrap();
let trace: PreStateFrame = serde_json::from_str(DIFF_MODE).unwrap();
match trace {
PreStateFrame::Diff(DiffMode { pre: _pre, post: _post }) => {}
_ => unreachable!(),
}
}
}
Loading