From 1e0603f9239deec110753ff57032f8b3cba3c4a9 Mon Sep 17 00:00:00 2001 From: James Kim Date: Wed, 10 Jul 2024 06:03:32 -0400 Subject: [PATCH] feat(anvil): add callTracer support for debug_traceCall (#8375) * 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 --- crates/anvil/core/src/eth/mod.rs | 4 +- crates/anvil/src/eth/api.rs | 10 +- crates/anvil/src/eth/backend/mem/inspector.rs | 5 + crates/anvil/src/eth/backend/mem/mod.rs | 68 +++++++++- crates/anvil/tests/it/traces.rs | 122 +++++++++++++++++- 5 files changed, 195 insertions(+), 14 deletions(-) diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index 8a832b79e857..8e3a8e59648e 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -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; @@ -297,7 +297,7 @@ pub enum EthRequest { DebugTraceCall( WithOtherFields, #[cfg_attr(feature = "serde", serde(default))] Option, - #[cfg_attr(feature = "serde", serde(default))] GethDefaultTracingOptions, + #[cfg_attr(feature = "serde", serde(default))] GethDebugTracingCallOptions, ), /// Trace transaction endpoint for parity's `trace_transaction` diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index a0cc94249134..346fb27bd1dd 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -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}, @@ -1523,8 +1523,8 @@ impl EthApi { &self, request: WithOtherFields, block_number: Option, - opts: GethDefaultTracingOptions, - ) -> Result { + opts: GethDebugTracingCallOptions, + ) -> Result { node_info!("debug_traceCall"); let block_request = self.block_request(block_number).await?; let fees = FeeDetails::new( @@ -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 = + self.backend.call_with_tracing(request, fees, Some(block_request), opts).await; + result } /// Returns traces for the transaction hash via parity's tracing endpoint diff --git a/crates/anvil/src/eth/backend/mem/inspector.rs b/crates/anvil/src/eth/backend/mem/inspector.rs index ed419dd98645..43ba00810b94 100644 --- a/crates/anvil/src/eth/backend/mem/inspector.rs +++ b/crates/anvil/src/eth/backend/mem/inspector.rs @@ -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())); diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 728535fc2e7e..9bb431e2e79f 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -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, @@ -73,6 +76,7 @@ use foundry_evm::{ TxEnv, KECCAK_EMPTY, }, }, + traces::TracingInspectorConfig, utils::new_evm_with_inspector_ref, InspectorExt, }; @@ -1223,12 +1227,58 @@ impl Backend { request: WithOtherFields, fee_details: FeeDetails, block_request: Option, - opts: GethDefaultTracingOptions, - ) -> Result { + opts: GethDebugTracingCallOptions, + ) -> Result { + 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()?; @@ -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? diff --git a/crates/anvil/tests/it/traces.rs b/crates/anvil/tests/it/traces.rs index ae75c261a8f1..8a4d2a83d0dd 100644 --- a/crates/anvil/tests/it/traces.rs +++ b/crates/anvil/tests/it/traces.rs @@ -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::{ @@ -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, @@ -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::>(); + 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!() + } + } +} + // #[tokio::test(flavor = "multi_thread")] async fn test_trace_address_fork() {