Skip to content

Commit

Permalink
feat(anvil): add callTracer support for debug_traceCall (#8375)
Browse files Browse the repository at this point in the history
* apply state/block overrides and add callTracer support

* remove state/block override logic

* pass default config to TracingInspector

* fix comments

* add integration tests

* refactor handler

* add comments

* fix clippy

* update test to check for address

---------

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
  • Loading branch information
jakim929 and mattsse committed Jul 10, 2024
1 parent 82ff8ee commit 1e0603f
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 14 deletions.
4 changes: 2 additions & 2 deletions crates/anvil/core/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use alloy_rpc_types::{
pubsub::{Params as SubscriptionParams, SubscriptionKind},
request::TransactionRequest,
state::StateOverride,
trace::geth::{GethDebugTracingOptions, GethDefaultTracingOptions},
trace::geth::{GethDebugTracingCallOptions, GethDebugTracingOptions},
BlockId, BlockNumberOrTag as BlockNumber, Filter, Index,
};
use alloy_serde::WithOtherFields;
Expand Down Expand Up @@ -297,7 +297,7 @@ pub enum EthRequest {
DebugTraceCall(
WithOtherFields<TransactionRequest>,
#[cfg_attr(feature = "serde", serde(default))] Option<BlockId>,
#[cfg_attr(feature = "serde", serde(default))] GethDefaultTracingOptions,
#[cfg_attr(feature = "serde", serde(default))] GethDebugTracingCallOptions,
),

/// Trace transaction endpoint for parity's `trace_transaction`
Expand Down
10 changes: 6 additions & 4 deletions crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use alloy_rpc_types::{
request::TransactionRequest,
state::StateOverride,
trace::{
geth::{DefaultFrame, GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace},
geth::{GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace},
parity::LocalizedTransactionTrace,
},
txpool::{TxpoolContent, TxpoolInspect, TxpoolInspectSummary, TxpoolStatus},
Expand Down Expand Up @@ -1523,8 +1523,8 @@ impl EthApi {
&self,
request: WithOtherFields<TransactionRequest>,
block_number: Option<BlockId>,
opts: GethDefaultTracingOptions,
) -> Result<DefaultFrame> {
opts: GethDebugTracingCallOptions,
) -> Result<GethTrace> {
node_info!("debug_traceCall");
let block_request = self.block_request(block_number).await?;
let fees = FeeDetails::new(
Expand All @@ -1535,7 +1535,9 @@ impl EthApi {
)?
.or_zero_fees();

self.backend.call_with_tracing(request, fees, Some(block_request), opts).await
let result: std::result::Result<GethTrace, BlockchainError> =
self.backend.call_with_tracing(request, fees, Some(block_request), opts).await;
result
}

/// Returns traces for the transaction hash via parity's tracing endpoint
Expand Down
5 changes: 5 additions & 0 deletions crates/anvil/src/eth/backend/mem/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ impl Inspector {
self
}

pub fn with_config(mut self, config: TracingInspectorConfig) -> Self {
self.tracer = Some(TracingInspector::new(config));
self
}

/// Enables steps recording for `Tracer`.
pub fn with_steps_tracing(mut self) -> Self {
self.tracer = Some(TracingInspector::new(TracingInspectorConfig::all()));
Expand Down
68 changes: 62 additions & 6 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ use alloy_rpc_types::{
serde_helpers::JsonStorageKey,
state::StateOverride,
trace::{
geth::{DefaultFrame, GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace},
geth::{
GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions,
GethDebugTracingOptions, GethTrace, NoopFrame,
},
parity::LocalizedTransactionTrace,
},
AccessList, Block as AlloyBlock, BlockId, BlockNumberOrTag as BlockNumber,
Expand Down Expand Up @@ -73,6 +76,7 @@ use foundry_evm::{
TxEnv, KECCAK_EMPTY,
},
},
traces::TracingInspectorConfig,
utils::new_evm_with_inspector_ref,
InspectorExt,
};
Expand Down Expand Up @@ -1223,12 +1227,58 @@ impl Backend {
request: WithOtherFields<TransactionRequest>,
fee_details: FeeDetails,
block_request: Option<BlockRequest>,
opts: GethDefaultTracingOptions,
) -> Result<DefaultFrame, BlockchainError> {
opts: GethDebugTracingCallOptions,
) -> Result<GethTrace, BlockchainError> {
let GethDebugTracingCallOptions { tracing_options, block_overrides: _, state_overrides: _ } =
opts;
let GethDebugTracingOptions { config, tracer, tracer_config, .. } = tracing_options;

self.with_database_at(block_request, |state, block| {
let mut inspector = Inspector::default().with_steps_tracing();
let block_number = block.number;

if let Some(tracer) = tracer {
return match tracer {
GethDebugTracerType::BuiltInTracer(tracer) => match tracer {
GethDebugBuiltInTracerType::CallTracer => {
let call_config = tracer_config
.into_call_config()
.map_err(|e| (RpcError::invalid_params(e.to_string())))?;

let mut inspector = Inspector::default().with_config(
TracingInspectorConfig::from_geth_call_config(&call_config),
);

let env = self.build_call_env(request, fee_details, block);
let mut evm =
self.new_evm_with_inspector_ref(state, env, &mut inspector);
let ResultAndState { result, state: _ } = evm.transact()?;

drop(evm);
let tracing_inspector = inspector.tracer.expect("tracer disappeared");

Ok(tracing_inspector
.into_geth_builder()
.geth_call_traces(call_config, result.gas_used())
.into())
}
GethDebugBuiltInTracerType::NoopTracer => Ok(NoopFrame::default().into()),
GethDebugBuiltInTracerType::FourByteTracer |
GethDebugBuiltInTracerType::PreStateTracer |
GethDebugBuiltInTracerType::MuxTracer => {
Err(RpcError::invalid_params("unsupported tracer type").into())
}
},

GethDebugTracerType::JsTracer(_code) => {
Err(RpcError::invalid_params("unsupported tracer type").into())
}
}
}

// defaults to StructLog tracer used since no tracer is specified
let mut inspector =
Inspector::default().with_config(TracingInspectorConfig::from_geth_config(&config));

let env = self.build_call_env(request, fee_details, block);
let mut evm = self.new_evm_with_inspector_ref(state, env, &mut inspector);
let ResultAndState { result, state: _ } = evm.transact()?;
Expand All @@ -1244,10 +1294,16 @@ impl Backend {
};

drop(evm);
let tracer = inspector.tracer.expect("tracer disappeared");
let tracing_inspector = inspector.tracer.expect("tracer disappeared");
let return_value = out.as_ref().map(|o| o.data().clone()).unwrap_or_default();
let res = tracer.into_geth_builder().geth_traces(gas_used, return_value, opts);

trace!(target: "backend", ?exit_reason, ?out, %gas_used, %block_number, "trace call");

let res = tracing_inspector
.into_geth_builder()
.geth_traces(gas_used, return_value, config)
.into();

Ok(res)
})
.await?
Expand Down
122 changes: 120 additions & 2 deletions crates/anvil/tests/it/traces.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::{fork::fork_config, utils::http_provider_with_signer};
use crate::{
abi::{MulticallContract, SimpleStorage},
fork::fork_config,
utils::http_provider_with_signer,
};
use alloy_network::{EthereumWallet, TransactionBuilder};
use alloy_primitives::{hex, Address, Bytes, U256};
use alloy_provider::{
Expand All @@ -7,7 +11,10 @@ use alloy_provider::{
};
use alloy_rpc_types::{
trace::{
geth::{GethDebugTracingCallOptions, GethTrace},
geth::{
CallConfig, GethDebugBuiltInTracerType, GethDebugTracerType,
GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace,
},
parity::{Action, LocalizedTransactionTrace},
},
BlockNumberOrTag, TransactionRequest,
Expand Down Expand Up @@ -138,6 +145,117 @@ async fn test_transfer_debug_trace_call() {
}
}

#[tokio::test(flavor = "multi_thread")]
async fn test_call_tracer_debug_trace_call() {
let (_api, handle) = spawn(NodeConfig::test()).await;
let wallets = handle.dev_wallets().collect::<Vec<_>>();
let deployer: EthereumWallet = wallets[0].clone().into();
let provider = http_provider_with_signer(&handle.http_endpoint(), deployer);

let multicall_contract = MulticallContract::deploy(&provider).await.unwrap();

let simple_storage_contract =
SimpleStorage::deploy(&provider, "init value".to_string()).await.unwrap();

let set_value = simple_storage_contract.setValue("bar".to_string());
let set_value_calldata = set_value.calldata();

let internal_call_tx_builder = multicall_contract.aggregate(vec![MulticallContract::Call {
target: *simple_storage_contract.address(),
callData: set_value_calldata.to_owned(),
}]);

let internal_call_tx_calldata = internal_call_tx_builder.calldata().to_owned();

// calling SimpleStorage contract through Multicall should result in an internal call
let internal_call_tx = TransactionRequest::default()
.from(wallets[1].address())
.to(*multicall_contract.address())
.with_input(internal_call_tx_calldata);

let internal_call_tx_traces = handle
.http_provider()
.debug_trace_call(
internal_call_tx.clone(),
BlockNumberOrTag::Latest,
GethDebugTracingCallOptions::default().with_tracing_options(
GethDebugTracingOptions::default()
.with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer))
.with_call_config(CallConfig::default().with_log()),
),
)
.await
.unwrap();

match internal_call_tx_traces {
GethTrace::CallTracer(call_frame) => {
assert!(call_frame.calls.len() == 1);
assert!(
call_frame.calls.first().unwrap().to.unwrap() == *simple_storage_contract.address()
);
assert!(call_frame.calls.first().unwrap().logs.len() == 1);
}
_ => {
unreachable!()
}
}

// only_top_call option - should not return any internal calls
let internal_call_only_top_call_tx_traces = handle
.http_provider()
.debug_trace_call(
internal_call_tx.clone(),
BlockNumberOrTag::Latest,
GethDebugTracingCallOptions::default().with_tracing_options(
GethDebugTracingOptions::default()
.with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer))
.with_call_config(CallConfig::default().with_log().only_top_call()),
),
)
.await
.unwrap();

match internal_call_only_top_call_tx_traces {
GethTrace::CallTracer(call_frame) => {
assert!(call_frame.calls.is_empty());
}
_ => {
unreachable!()
}
}

// directly calling the SimpleStorage contract should not result in any internal calls
let direct_call_tx = TransactionRequest::default()
.from(wallets[1].address())
.to(*simple_storage_contract.address())
.with_input(set_value_calldata.to_owned());

let direct_call_tx_traces = handle
.http_provider()
.debug_trace_call(
direct_call_tx,
BlockNumberOrTag::Latest,
GethDebugTracingCallOptions::default().with_tracing_options(
GethDebugTracingOptions::default()
.with_tracer(GethDebugTracerType::from(GethDebugBuiltInTracerType::CallTracer))
.with_call_config(CallConfig::default().with_log()),
),
)
.await
.unwrap();

match direct_call_tx_traces {
GethTrace::CallTracer(call_frame) => {
assert!(call_frame.calls.is_empty());
assert!(call_frame.to.unwrap() == *simple_storage_contract.address());
assert!(call_frame.logs.len() == 1);
}
_ => {
unreachable!()
}
}
}

// <https://github.com/foundry-rs/foundry/issues/2656>
#[tokio::test(flavor = "multi_thread")]
async fn test_trace_address_fork() {
Expand Down

0 comments on commit 1e0603f

Please sign in to comment.