From f120764dd9ffcc259e85c50a6b6da60750c79f74 Mon Sep 17 00:00:00 2001 From: Alex Pikme <30472093+reject-i@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:03:58 +0100 Subject: [PATCH] feat: upstream DebugInspector from reth --- src/tracing/debug.rs | 325 +++++++++++++++++++++++++++++++++++++++++++ src/tracing/mod.rs | 3 + 2 files changed, 328 insertions(+) create mode 100644 src/tracing/debug.rs diff --git a/src/tracing/debug.rs b/src/tracing/debug.rs new file mode 100644 index 00000000..57e77850 --- /dev/null +++ b/src/tracing/debug.rs @@ -0,0 +1,325 @@ +use crate::tracing::{ + FourByteInspector, MuxInspector, TracingInspector, TracingInspectorConfig, TransactionContext, +}; +#[cfg(feature = "js-tracer")] +use alloc::{boxed::Box, string::String}; +use alloy_rpc_types_eth::TransactionInfo; +use alloy_rpc_types_trace::geth::{ + mux::MuxConfig, CallConfig, FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace, NoopFrame, PreStateConfig, +}; +use revm::{ + context_interface::{ + result::{HaltReasonTr, ResultAndState}, + Block, ContextTr, Transaction, + }, + inspector::JournalExt, + interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter}, + primitives::{Address, Log, U256}, + DatabaseRef, Inspector, +}; +use thiserror::Error; + +/// Inspector for the `debug` API +/// +/// This inspector is used to trace the execution of a transaction or call and supports all variants +/// of [`GethDebugTracerType`]. +/// +/// This inspector can be re-used for tracing multiple transactions. This is supported by +/// requiring caller to invoke [`DebugInspector::fuse`] after each transaction. See method +/// documentation for more details. +#[derive(Debug)] +pub enum DebugInspector { + /// FourByte tracer + FourByte(FourByteInspector), + /// CallTracer + CallTracer(TracingInspector, CallConfig), + /// PreStateTracer + PreStateTracer(TracingInspector, PreStateConfig), + /// Noop tracer + Noop(revm::inspector::NoOpInspector), + /// Mux tracer + Mux(MuxInspector, MuxConfig), + /// FlatCallTracer + FlatCallTracer(TracingInspector), + /// Default tracer + Default(TracingInspector, GethDefaultTracingOptions), + #[cfg(feature = "js-tracer")] + /// JS tracer + Js(Box, String, serde_json::Value), +} + +impl DebugInspector { + /// Create a new `DebugInspector` from the given tracing options. + pub fn new(opts: GethDebugTracingOptions) -> Result { + let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts; + + let this = if let Some(tracer) = tracer { + #[allow(unreachable_patterns)] + match tracer { + GethDebugTracerType::BuiltInTracer(tracer) => match tracer { + GethDebugBuiltInTracerType::FourByteTracer => { + Self::FourByte(FourByteInspector::default()) + } + GethDebugBuiltInTracerType::CallTracer => { + let config = tracer_config + .into_call_config() + .map_err(|_| DebugInspectorError::InvalidTracerConfig)?; + + Self::CallTracer( + TracingInspector::new(TracingInspectorConfig::from_geth_call_config( + &config, + )), + config, + ) + } + GethDebugBuiltInTracerType::PreStateTracer => { + let config = tracer_config + .into_pre_state_config() + .map_err(|_| DebugInspectorError::InvalidTracerConfig)?; + + Self::PreStateTracer( + TracingInspector::new( + TracingInspectorConfig::from_geth_prestate_config(&config), + ), + config, + ) + } + GethDebugBuiltInTracerType::NoopTracer => { + Self::Noop(revm::inspector::NoOpInspector) + } + GethDebugBuiltInTracerType::MuxTracer => { + let config = tracer_config + .into_mux_config() + .map_err(|_| DebugInspectorError::InvalidTracerConfig)?; + + Self::Mux(MuxInspector::try_from_config(config.clone())?, config) + } + GethDebugBuiltInTracerType::FlatCallTracer => { + let flat_call_config = tracer_config + .into_flat_call_config() + .map_err(|_| DebugInspectorError::InvalidTracerConfig)?; + + Self::FlatCallTracer(TracingInspector::new( + TracingInspectorConfig::from_flat_call_config(&flat_call_config), + )) + } + _ => { + // Note: this match is non-exhaustive in case we need to add support for + // additional tracers + return Err(DebugInspectorError::UnsupportedTracer); + } + }, + #[cfg(not(feature = "js-tracer"))] + GethDebugTracerType::JsTracer(_) => { + return Err(DebugInspectorError::JsTracerNotEnabled); + } + #[cfg(feature = "js-tracer")] + GethDebugTracerType::JsTracer(code) => { + let config = tracer_config.into_json(); + Self::Js( + crate::tracing::js::JsInspector::new(code.clone(), config.clone())?.into(), + code, + config, + ) + } + _ => { + // Note: this match is non-exhaustive in case we need to add support for + // additional tracers + return Err(DebugInspectorError::UnsupportedTracer); + } + } + } else { + Self::Default( + TracingInspector::new(TracingInspectorConfig::from_geth_config(&config)), + config, + ) + }; + + Ok(this) + } + + /// Prepares inspector for executing the next transaction. This will remove any state from + /// previous transactions. + pub fn fuse(&mut self) -> Result<(), DebugInspectorError> { + match self { + Self::FourByte(inspector) => { + core::mem::take(inspector); + } + Self::CallTracer(inspector, _) + | Self::PreStateTracer(inspector, _) + | Self::FlatCallTracer(inspector) + | Self::Default(inspector, _) => inspector.fuse(), + Self::Noop(_) => {} + Self::Mux(inspector, config) => { + *inspector = MuxInspector::try_from_config(config.clone())?; + } + #[cfg(feature = "js-tracer")] + Self::Js(inspector, code, config) => { + *inspector = + crate::tracing::js::JsInspector::new(code.clone(), config.clone())?.into(); + } + } + + Ok(()) + } + + /// Should be invoked after each transaction to obtain the resulting [`GethTrace`]. + pub fn get_result( + &mut self, + tx_context: Option, + tx_env: &impl Transaction, + block_env: &impl Block, + res: &ResultAndState, + db: &mut DB, + ) -> Result> { + let tx_info = TransactionInfo { + hash: tx_context.as_ref().and_then(|c| c.tx_hash), + index: tx_context.as_ref().and_then(|c| c.tx_index.map(|i| i as u64)), + block_hash: tx_context.as_ref().and_then(|c| c.block_hash), + block_number: Some(block_env.number().saturating_to()), + base_fee: Some(block_env.basefee()), + }; + + let res = match self { + Self::FourByte(inspector) => FourByteFrame::from(&*inspector).into(), + Self::CallTracer(inspector, config) => { + inspector.set_transaction_gas_limit(tx_env.gas_limit()); + inspector.geth_builder().geth_call_traces(*config, res.result.gas_used()).into() + } + Self::PreStateTracer(inspector, config) => { + inspector.set_transaction_gas_limit(tx_env.gas_limit()); + inspector + .geth_builder() + .geth_prestate_traces(res, config, db) + .map_err(DebugInspectorError::Database)? + .into() + } + Self::Noop(_) => NoopFrame::default().into(), + Self::Mux(inspector, _) => inspector + .try_into_mux_frame(res, db, tx_info) + .map_err(DebugInspectorError::Database)? + .into(), + Self::FlatCallTracer(inspector) => { + inspector.set_transaction_gas_limit(tx_env.gas_limit()); + inspector + .clone() + .into_parity_builder() + .into_localized_transaction_traces(tx_info) + .into() + } + Self::Default(inspector, config) => { + inspector.set_transaction_gas_limit(tx_env.gas_limit()); + inspector + .geth_builder() + .geth_traces( + res.result.gas_used(), + res.result.output().unwrap_or_default().clone(), + *config, + ) + .into() + } + #[cfg(feature = "js-tracer")] + Self::Js(inspector, _, _) => { + inspector.set_transaction_context(tx_context.unwrap_or_default()); + let res = inspector + .json_result(res.clone(), tx_env, block_env, db) + .map_err(DebugInspectorError::JsInspector)?; + + GethTrace::JS(res) + } + }; + + Ok(res) + } +} + +macro_rules! delegate { + ($self:expr => $insp:ident.$method:ident($($arg:expr),*)) => { + match $self { + Self::FourByte($insp) => Inspector::::$method($insp, $($arg),*), + Self::CallTracer($insp, _) => Inspector::::$method($insp, $($arg),*), + Self::PreStateTracer($insp, _) => Inspector::::$method($insp, $($arg),*), + Self::FlatCallTracer($insp) => Inspector::::$method($insp, $($arg),*), + Self::Default($insp, _) => Inspector::::$method($insp, $($arg),*), + Self::Noop($insp) => Inspector::::$method($insp, $($arg),*), + Self::Mux($insp, _) => Inspector::::$method($insp, $($arg),*), + #[cfg(feature = "js-tracer")] + Self::Js($insp, _, _) => Inspector::::$method($insp, $($arg),*), + } + }; +} + +impl Inspector for DebugInspector +where + CTX: ContextTr, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + delegate!(self => inspector.initialize_interp(interp, context)) + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + delegate!(self => inspector.step(interp, context)) + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + delegate!(self => inspector.step_end(interp, context)) + } + + fn log(&mut self, context: &mut CTX, log: Log) { + delegate!(self => inspector.log(context, log)) + } + + fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + delegate!(self => inspector.log_full(interp, context, log)) + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + delegate!(self => inspector.call(context, inputs)) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + delegate!(self => inspector.call_end(context, inputs, outcome)) + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + delegate!(self => inspector.create(context, inputs)) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + delegate!(self => inspector.create_end(context, inputs, outcome)) + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + delegate!(self => inspector.selfdestruct(contract, target, value)) + } +} + +/// Error type for [DebugInspector] +#[derive(Debug, Error)] +pub enum DebugInspectorError { + /// Invalid tracer configuration + #[error("invalid tracer config")] + InvalidTracerConfig, + /// Unsupported tracer + #[error("unsupported tracer")] + UnsupportedTracer, + /// JS tracer is not enabled + #[error("JS Tracer is not enabled")] + JsTracerNotEnabled, + /// Error from MuxInspector + #[error(transparent)] + MuxInspector(#[from] crate::tracing::MuxError), + /// Error from JS inspector + #[cfg(feature = "js-tracer")] + #[error(transparent)] + JsInspector(#[from] crate::tracing::js::JsInspectorError), + /// Database error + #[error("database error: {0}")] + Database(DBError), +} diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs index be5fc158..6e6c407e 100644 --- a/src/tracing/mod.rs +++ b/src/tracing/mod.rs @@ -59,6 +59,9 @@ pub mod js; mod mux; pub use mux::{Error as MuxError, MuxInspector}; +mod debug; +pub use debug::{DebugInspector, DebugInspectorError}; + /// An inspector that collects call traces. /// /// This [Inspector] can be hooked into revm's EVM which then calls the inspector