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(anvil): add callTracer support for debug_traceCall #8375

Merged
merged 12 commits into from
Jul 10, 2024
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
Loading