diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index e42dd03721df6..0637430508823 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -8,7 +8,7 @@ use crate::{ eth::{ backend::{ cheats::{CheatEcrecover, CheatsManager}, - db::{Db, MaybeFullDatabase, SerializableState}, + db::{Db, MaybeFullDatabase, SerializableState, StateDb}, env::Env, executor::{ExecutedTransactions, TransactionExecutor}, fork::ClientFork, @@ -71,8 +71,8 @@ use alloy_rpc_types::{ trace::{ filter::TraceFilter, geth::{ - GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions, - GethDebugTracingOptions, GethTrace, NoopFrame, + FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, NoopFrame, }, parity::LocalizedTransactionTrace, }, @@ -99,7 +99,10 @@ use foundry_evm::{ constants::DEFAULT_CREATE2_DEPLOYER_RUNTIME_CODE, decode::RevertDecoder, inspectors::AccessListInspector, - traces::{CallTraceDecoder, TracingInspectorConfig}, + traces::{ + CallTraceDecoder, FourByteInspector, GethTraceBuilder, TracingInspector, + TracingInspectorConfig, + }, utils::{get_blob_base_fee_update_fraction, get_blob_base_fee_update_fraction_by_spec_id}, }; use foundry_evm_core::{either_evm::EitherEvm, precompiles::EC_RECOVER}; @@ -1945,9 +1948,31 @@ impl Backend { .geth_call_traces(call_config, result.gas_used()) .into()) } + GethDebugBuiltInTracerType::PreStateTracer => { + let pre_state_config = tracer_config + .into_pre_state_config() + .map_err(|e| RpcError::invalid_params(e.to_string()))?; + + let mut inspector = TracingInspector::new( + TracingInspectorConfig::from_geth_prestate_config( + &pre_state_config, + ), + ); + + let env = self.build_call_env(request, fee_details, block); + let mut evm = + self.new_evm_with_inspector_ref(&cache_db, &env, &mut inspector); + let result = evm.transact(env.tx)?; + + drop(evm); + + Ok(inspector + .into_geth_builder() + .geth_prestate_traces(&result, &pre_state_config, cache_db)? + .into()) + } GethDebugBuiltInTracerType::NoopTracer => Ok(NoopFrame::default().into()), GethDebugBuiltInTracerType::FourByteTracer - | GethDebugBuiltInTracerType::PreStateTracer | GethDebugBuiltInTracerType::MuxTracer | GethDebugBuiltInTracerType::FlatCallTracer => { Err(RpcError::invalid_params("unsupported tracer type").into()) @@ -2634,7 +2659,7 @@ impl Backend { .await; } - if let Some(trace) = self.mined_geth_trace_transaction(hash, opts.clone()) { + if let Some(trace) = self.mined_geth_trace_transaction(hash, opts.clone()).await { return trace; } @@ -2645,16 +2670,19 @@ impl Backend { Ok(GethTrace::Default(Default::default())) } - /// Traces the transaction with the js tracer - #[cfg(feature = "js-tracer")] - pub async fn trace_tx_with_js_tracer( + fn replay_tx_with_inspector( &self, hash: B256, - code: String, - opts: GethDebugTracingOptions, - ) -> Result { - let GethDebugTracingOptions { tracer_config, .. } = opts; - + mut inspector: I, + f: F, + ) -> Result + where + for<'a> I: Inspector>>>> + + Inspector>>>> + + 'a, + for<'a> F: + FnOnce(ResultAndState, CacheDB>, I, TxEnv, Env) -> T, + { let block = { let storage = self.blockchain.storage.read(); let MinedTransaction { block_hash, .. } = storage @@ -2730,23 +2758,41 @@ impl Backend { let target_tx = PendingTransaction::from_maybe_impersonated(target_tx)?; let tx_env = target_tx.to_revm_tx_env(); - let config = tracer_config.into_json(); - let mut inspector = revm_inspectors::tracing::js::JsInspector::new(code, config) - .map_err(|err| BlockchainError::Message(err.to_string()))?; let mut evm = self.new_evm_with_inspector_ref(&cache_db, &env, &mut inspector); let result = evm .transact(tx_env.clone()) .map_err(|err| BlockchainError::Message(err.to_string()))?; - let trace = inspector - .json_result( - result, - &alloy_evm::IntoTxEnv::into_tx_env(tx_env), - &env.evm_env.block_env, - &cache_db, - ) - .map_err(|e| BlockchainError::Message(e.to_string()))?; + Ok(f(result, cache_db, inspector, tx_env.base, env)) + } + + /// Traces the transaction with the js tracer + #[cfg(feature = "js-tracer")] + pub async fn trace_tx_with_js_tracer( + &self, + hash: B256, + code: String, + opts: GethDebugTracingOptions, + ) -> Result { + let GethDebugTracingOptions { tracer_config, .. } = opts; + let config = tracer_config.into_json(); + let inspector = revm_inspectors::tracing::js::JsInspector::new(code, config) + .map_err(|err| BlockchainError::Message(err.to_string()))?; + let trace = self.replay_tx_with_inspector( + hash, + inspector, + |result, cache_db, mut inspector, tx_env, env| { + inspector + .json_result( + result, + &alloy_evm::IntoTxEnv::into_tx_env(tx_env), + &env.evm_env.block_env, + &cache_db, + ) + .map_err(|e| BlockchainError::Message(e.to_string())) + }, + )??; Ok(GethTrace::JS(trace)) } @@ -2766,12 +2812,99 @@ impl Backend { Ok(None) } - fn mined_geth_trace_transaction( + fn geth_trace( + &self, + tx: &MinedTransaction, + opts: GethDebugTracingOptions, + ) -> Result { + let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts; + + if let Some(tracer) = tracer { + match tracer { + GethDebugTracerType::BuiltInTracer(tracer) => match tracer { + GethDebugBuiltInTracerType::FourByteTracer => { + let inspector = FourByteInspector::default(); + let res = self.replay_tx_with_inspector( + tx.info.transaction_hash, + inspector, + |_, _, inspector, _, _| FourByteFrame::from(inspector).into(), + )?; + return Ok(res); + } + GethDebugBuiltInTracerType::CallTracer => { + return match tracer_config.into_call_config() { + Ok(call_config) => { + let inspector = TracingInspector::new( + TracingInspectorConfig::from_geth_call_config(&call_config), + ); + let frame = self.replay_tx_with_inspector( + tx.info.transaction_hash, + inspector, + |_, _, inspector, _, _| { + inspector + .geth_builder() + .geth_call_traces( + call_config, + tx.receipt.cumulative_gas_used(), + ) + .into() + }, + )?; + Ok(frame) + } + Err(e) => Err(RpcError::invalid_params(e.to_string()).into()), + }; + } + GethDebugBuiltInTracerType::PreStateTracer => { + return match tracer_config.into_pre_state_config() { + Ok(pre_state_config) => { + let inspector = TracingInspector::new( + TracingInspectorConfig::from_geth_prestate_config( + &pre_state_config, + ), + ); + let frame = self.replay_tx_with_inspector( + tx.info.transaction_hash, + inspector, + |state, db, inspector, _, _| { + inspector.geth_builder().geth_prestate_traces( + &state, + &pre_state_config, + db, + ) + }, + )??; + Ok(frame.into()) + } + Err(e) => Err(RpcError::invalid_params(e.to_string()).into()), + }; + } + GethDebugBuiltInTracerType::NoopTracer + | GethDebugBuiltInTracerType::MuxTracer + | GethDebugBuiltInTracerType::FlatCallTracer => {} + }, + GethDebugTracerType::JsTracer(_code) => {} + } + + return Ok(NoopFrame::default().into()); + } + + // default structlog tracer + Ok(GethTraceBuilder::new(tx.info.traces.clone()) + .geth_traces( + tx.receipt.cumulative_gas_used(), + tx.info.out.clone().unwrap_or_default(), + config, + ) + .into()) + } + + async fn mined_geth_trace_transaction( &self, hash: B256, opts: GethDebugTracingOptions, ) -> Option> { - self.blockchain.storage.read().transactions.get(&hash).map(|tx| tx.geth_trace(opts)) + self.blockchain.storage.read().transactions.get(&hash).map(|tx| self.geth_trace(tx, opts)) } /// Returns the traces for the given block diff --git a/crates/anvil/src/eth/backend/mem/storage.rs b/crates/anvil/src/eth/backend/mem/storage.rs index 83349ea049b05..3e301e1b4be9e 100644 --- a/crates/anvil/src/eth/backend/mem/storage.rs +++ b/crates/anvil/src/eth/backend/mem/storage.rs @@ -8,7 +8,6 @@ use crate::eth::{ env::Env, mem::cache::DiskStateCache, }, - error::BlockchainError, pool::transactions::PoolTransaction, }; use alloy_consensus::constants::EMPTY_WITHDRAWALS; @@ -20,10 +19,6 @@ use alloy_primitives::{ use alloy_rpc_types::{ BlockId, BlockNumberOrTag, TransactionInfo as RethTransactionInfo, trace::{ - geth::{ - FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType, - GethDebugTracingOptions, GethTrace, NoopFrame, - }, otterscan::{InternalOperation, OperationType}, parity::LocalizedTransactionTrace, }, @@ -32,12 +27,9 @@ use anvil_core::eth::{ block::{Block, PartialHeader}, transaction::{MaybeImpersonatedTransaction, ReceiptResponse, TransactionInfo, TypedReceipt}, }; -use anvil_rpc::error::RpcError; use foundry_evm::{ backend::MemDb, - traces::{ - CallKind, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, TracingInspectorConfig, - }, + traces::{CallKind, ParityTraceBuilder, TracingInspectorConfig}, }; use parking_lot::RwLock; use revm::{context::Block as RevmBlock, primitives::hardfork::SpecId}; @@ -558,45 +550,6 @@ impl MinedTransaction { }) .collect() } - - pub fn geth_trace(&self, opts: GethDebugTracingOptions) -> Result { - let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts; - - if let Some(tracer) = tracer { - match tracer { - GethDebugTracerType::BuiltInTracer(tracer) => match tracer { - GethDebugBuiltInTracerType::FourByteTracer => { - let inspector = FourByteInspector::default(); - return Ok(FourByteFrame::from(inspector).into()); - } - GethDebugBuiltInTracerType::CallTracer => { - return match tracer_config.into_call_config() { - Ok(call_config) => Ok(GethTraceBuilder::new(self.info.traces.clone()) - .geth_call_traces(call_config, self.receipt.cumulative_gas_used()) - .into()), - Err(e) => Err(RpcError::invalid_params(e.to_string()).into()), - }; - } - GethDebugBuiltInTracerType::PreStateTracer - | GethDebugBuiltInTracerType::NoopTracer - | GethDebugBuiltInTracerType::MuxTracer - | GethDebugBuiltInTracerType::FlatCallTracer => {} - }, - GethDebugTracerType::JsTracer(_code) => {} - } - - return Ok(NoopFrame::default().into()); - } - - // default structlog tracer - Ok(GethTraceBuilder::new(self.info.traces.clone()) - .geth_traces( - self.receipt.cumulative_gas_used(), - self.info.out.clone().unwrap_or_default(), - config, - ) - .into()) - } } /// Intermediary Anvil representation of a receipt diff --git a/crates/anvil/tests/it/traces.rs b/crates/anvil/tests/it/traces.rs index 6fa60bb7a5b9b..26969b9c46927 100644 --- a/crates/anvil/tests/it/traces.rs +++ b/crates/anvil/tests/it/traces.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::{ abi::{Multicall, SimpleStorage}, fork::fork_config, @@ -20,8 +22,9 @@ use alloy_rpc_types::{ trace::{ filter::{TraceFilter, TraceFilterMode}, geth::{ - CallConfig, GethDebugBuiltInTracerType, GethDebugTracerType, - GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, + AccountState, CallConfig, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, PreStateConfig, + PreStateFrame, }, parity::{Action, LocalizedTransactionTrace}, }, @@ -1103,3 +1106,171 @@ fault: function(log) {} assert_eq!(actual, expected); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_call_tracer_debug_trace_call_pre_state_tracer() { + 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 = Multicall::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![Multicall::Call { + target: *simple_storage_contract.address(), + callData: set_value_calldata.to_owned(), + }]); + + let internal_call_tx_calldata = internal_call_tx_builder.calldata().to_owned(); + + let internal_call_tx = TransactionRequest::default() + .from(wallets[1].address()) + .to(*multicall_contract.address()) + .with_input(internal_call_tx_calldata); + + let result = api + .debug_trace_call( + WithOtherFields::new(internal_call_tx), + Some(BlockId::latest()), + GethDebugTracingCallOptions::default().with_tracing_options( + GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()), + ), + ) + .await + .unwrap(); + + let expected = r#" +{ + "0x0000000000000000000000000000000000000000": { + "balance": "0x12670f" + }, + "0x5fbdb2315678afecb367f032d93f642f64180aa3": { + "balance": "0x0", + "nonce": 1 + }, + "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { + "balance": "0x56bc75e2d63100000" + }, + "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512": { + "balance": "0x0", + "nonce": 1, + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x696e69742076616c756500000000000000000000000000000000000000000014", + "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } +} + "#; + let expected: HashMap = serde_json::from_str(expected).unwrap(); + + match result { + GethTrace::PreStateTracer(PreStateFrame::Default(pre_state_mode)) => { + for (addr, acc) in pre_state_mode.0 { + let expected_acc = expected.get(&addr).unwrap(); + assert_eq!(acc.balance, expected_acc.balance); + assert_eq!(acc.nonce, expected_acc.nonce); + let expected_storage = &expected_acc.storage; + for (slot, value) in acc.storage { + assert_eq!(value, *expected_storage.get(&slot).unwrap()) + } + } + } + _ => unreachable!(), + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_debug_trace_transaction_pre_state_tracer() { + let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into())); + let (api, handle) = spawn(node_config).await; + let provider = crate::utils::http_provider(&handle.http_endpoint()); + + let wallets = handle.dev_wallets().collect::>(); + let from = wallets[0].address(); + api.anvil_add_balance(from, U256::MAX).await.unwrap(); + api.anvil_add_balance(wallets[1].address(), U256::MAX).await.unwrap(); + + let multicall_contract = Multicall::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![Multicall::Call { + target: *simple_storage_contract.address(), + callData: set_value_calldata.to_owned(), + }]); + + let internal_call_tx_calldata = internal_call_tx_builder.calldata().to_owned(); + + let internal_call_tx = TransactionRequest::default() + .from(wallets[1].address()) + .to(*multicall_contract.address()) + .with_input(internal_call_tx_calldata) + .with_gas_limit(1_000_000) + .with_max_fee_per_gas(100_000_000_000) + .with_max_priority_fee_per_gas(100_000_000_000); + + let receipt = provider + .send_transaction(internal_call_tx.into()) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + let result = api + .debug_trace_transaction( + receipt.transaction_hash, + GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()), + ) + .await + .unwrap(); + + let expected = r#" +{ + "0x0000000000000000000000000000000000000000": { + "balance": "0x0" + }, + "0x5fbdb2315678afecb367f032d93f642f64180aa3": { + "balance": "0x0", + "nonce": 1 + }, + "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { + "balance": "0x56bc75e2d630fffff" + }, + "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512": { + "balance": "0x0", + "nonce": 1, + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x696e69742076616c756500000000000000000000000000000000000000000014", + "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } +} + "#; + let expected: HashMap = serde_json::from_str(expected).unwrap(); + + match result { + GethTrace::PreStateTracer(PreStateFrame::Default(pre_state_mode)) => { + for (addr, acc) in pre_state_mode.0 { + let expected_acc = expected.get(&addr).unwrap(); + assert_eq!(acc.balance, expected_acc.balance); + assert_eq!(acc.nonce, expected_acc.nonce); + let expected_storage = &expected_acc.storage; + for (slot, value) in acc.storage { + assert_eq!(value, *expected_storage.get(&slot).unwrap()) + } + } + } + _ => unreachable!(), + } +}