diff --git a/Cargo.lock b/Cargo.lock index 501d69bc2f..3abe264cc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4880,7 +4880,9 @@ version = "0.1.0" dependencies = [ "hashbrown 0.13.2", "reth-primitives", + "reth-rpc-types", "revm", + "serde", ] [[package]] @@ -5019,6 +5021,7 @@ dependencies = [ name = "reth-rpc-types" version = "0.1.0" dependencies = [ + "ethers-core", "jsonrpsee-types", "lru 0.9.0", "rand 0.8.5", diff --git a/crates/revm/revm-inspectors/Cargo.toml b/crates/revm/revm-inspectors/Cargo.toml index 5f21a007b2..19e5c34f70 100644 --- a/crates/revm/revm-inspectors/Cargo.toml +++ b/crates/revm/revm-inspectors/Cargo.toml @@ -9,7 +9,10 @@ description = "revm inspector implementations used by reth" [dependencies] # reth reth-primitives = { path = "../../primitives" } +reth-rpc-types = { path = "../../rpc/rpc-types" } revm = { version = "3.0.0" } # remove from reth and reexport from revm -hashbrown = "0.13" \ No newline at end of file +hashbrown = "0.13" + +serde = { version = "1.0", features = ["derive"] } diff --git a/crates/revm/revm-inspectors/src/lib.rs b/crates/revm/revm-inspectors/src/lib.rs index b4d79c4060..a447c70951 100644 --- a/crates/revm/revm-inspectors/src/lib.rs +++ b/crates/revm/revm-inspectors/src/lib.rs @@ -14,3 +14,6 @@ pub mod access_list; /// each inspector and allowing to hook on block/transaciton execution, /// used in the main RETH executor. pub mod stack; + +/// An inspector for recording traces +pub mod tracing; diff --git a/crates/revm/revm-inspectors/src/stack/maybe_owned.rs b/crates/revm/revm-inspectors/src/stack/maybe_owned.rs new file mode 100644 index 0000000000..897546f167 --- /dev/null +++ b/crates/revm/revm-inspectors/src/stack/maybe_owned.rs @@ -0,0 +1,209 @@ +use revm::{ + interpreter::{CallInputs, CreateInputs, Gas, InstructionResult, Interpreter}, + primitives::{db::Database, Bytes, B160, B256}, + EVMData, Inspector, +}; +use std::{ + cell::{Ref, RefCell}, + rc::Rc, +}; + +/// An [Inspector] that is either owned by an individual [Inspector] or is shared as part of a +/// series of inspectors in a [InspectorStack](crate::stack::InspectorStack). +/// +/// Caution: if the [Inspector] is _stacked_ then it _must_ be called first. +#[derive(Debug)] +pub enum MaybeOwnedInspector { + /// Inspector is owned. + Owned(Rc>), + /// Inspector is shared and part of a stack + Stacked(Rc>), +} + +impl MaybeOwnedInspector { + /// Create a new _owned_ instance + pub fn new_owned(inspector: INSP) -> Self { + MaybeOwnedInspector::Owned(Rc::new(RefCell::new(inspector))) + } + + /// Creates a [MaybeOwnedInspector::Stacked] clone of this type. + pub fn clone_stacked(&self) -> Self { + match self { + MaybeOwnedInspector::Owned(gas) | MaybeOwnedInspector::Stacked(gas) => { + MaybeOwnedInspector::Stacked(Rc::clone(gas)) + } + } + } + + /// Returns a reference to the inspector. + pub fn as_ref(&self) -> Ref<'_, INSP> { + match self { + MaybeOwnedInspector::Owned(insp) => insp.borrow(), + MaybeOwnedInspector::Stacked(insp) => insp.borrow(), + } + } +} + +impl MaybeOwnedInspector { + /// Create a new _owned_ instance + pub fn owned() -> Self { + Self::new_owned(Default::default()) + } +} + +impl Default for MaybeOwnedInspector { + fn default() -> Self { + Self::owned() + } +} + +impl Clone for MaybeOwnedInspector { + fn clone(&self) -> Self { + self.clone_stacked() + } +} + +impl Inspector for MaybeOwnedInspector +where + DB: Database, + INSP: Inspector, +{ + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + ) -> InstructionResult { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().initialize_interp(interp, data, is_static) + } + MaybeOwnedInspector::Stacked(_) => {} + } + + InstructionResult::Continue + } + + fn step( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + ) -> InstructionResult { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().step(interp, data, is_static) + } + MaybeOwnedInspector::Stacked(_) => {} + } + + InstructionResult::Continue + } + + fn log( + &mut self, + evm_data: &mut EVMData<'_, DB>, + address: &B160, + topics: &[B256], + data: &Bytes, + ) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().log(evm_data, address, topics, data) + } + MaybeOwnedInspector::Stacked(_) => {} + } + } + + fn step_end( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + eval: InstructionResult, + ) -> InstructionResult { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().step_end(interp, data, is_static, eval) + } + MaybeOwnedInspector::Stacked(_) => {} + } + + InstructionResult::Continue + } + + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CallInputs, + is_static: bool, + ) -> (InstructionResult, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().call(data, inputs, is_static) + } + MaybeOwnedInspector::Stacked(_) => {} + } + + (InstructionResult::Continue, Gas::new(0), Bytes::new()) + } + + fn call_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CallInputs, + remaining_gas: Gas, + ret: InstructionResult, + out: Bytes, + is_static: bool, + ) -> (InstructionResult, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().call_end(data, inputs, remaining_gas, ret, out, is_static) + } + MaybeOwnedInspector::Stacked(_) => {} + } + (ret, remaining_gas, out) + } + + fn create( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CreateInputs, + ) -> (InstructionResult, Option, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => return insp.borrow_mut().create(data, inputs), + MaybeOwnedInspector::Stacked(_) => {} + } + + (InstructionResult::Continue, None, Gas::new(0), Bytes::default()) + } + + fn create_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CreateInputs, + ret: InstructionResult, + address: Option, + remaining_gas: Gas, + out: Bytes, + ) -> (InstructionResult, Option, Gas, Bytes) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().create_end(data, inputs, ret, address, remaining_gas, out) + } + MaybeOwnedInspector::Stacked(_) => {} + } + + (ret, address, remaining_gas, out) + } + + fn selfdestruct(&mut self, contract: B160, target: B160) { + match self { + MaybeOwnedInspector::Owned(insp) => { + return insp.borrow_mut().selfdestruct(contract, target) + } + MaybeOwnedInspector::Stacked(_) => {} + } + } +} diff --git a/crates/revm/revm-inspectors/src/stack.rs b/crates/revm/revm-inspectors/src/stack/mod.rs similarity index 98% rename from crates/revm/revm-inspectors/src/stack.rs rename to crates/revm/revm-inspectors/src/stack/mod.rs index 01d56c8703..5a37169e2f 100644 --- a/crates/revm/revm-inspectors/src/stack.rs +++ b/crates/revm/revm-inspectors/src/stack/mod.rs @@ -6,6 +6,10 @@ use revm::{ Database, EVMData, Inspector, }; +/// A wrapped [Inspector](revm::Inspector) that can be reused in the stack +mod maybe_owned; +pub use maybe_owned::MaybeOwnedInspector; + /// One can hook on inspector execution in 3 ways: /// - Block: Hook on block execution /// - BlockWithIndex: Hook on block execution transaction index diff --git a/crates/revm/revm-inspectors/src/tracing/arena.rs b/crates/revm/revm-inspectors/src/tracing/arena.rs new file mode 100644 index 0000000000..08bfa65bf9 --- /dev/null +++ b/crates/revm/revm-inspectors/src/tracing/arena.rs @@ -0,0 +1,160 @@ +use crate::tracing::types::{CallTrace, CallTraceNode, LogCallOrder}; +use reth_primitives::{Address, JsonU256, H256, U256}; +use reth_rpc_types::trace::{ + geth::{DefaultFrame, GethDebugTracingOptions, StructLog}, + parity::{ActionType, TransactionTrace}, +}; +use revm::interpreter::{opcode, InstructionResult}; +use std::collections::{BTreeMap, HashMap}; + +/// An arena of recorded traces. +/// +/// This type will be populated via the [TracingInspector](crate::tracing::TracingInspector). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CallTraceArena { + /// The arena of recorded trace nodes + pub(crate) arena: Vec, +} + +impl CallTraceArena { + /// Pushes a new trace into the arena, returning the trace ID + pub(crate) fn push_trace(&mut self, entry: usize, new_trace: CallTrace) -> usize { + match new_trace.depth { + // The entry node, just update it + 0 => { + self.arena[0].trace = new_trace; + 0 + } + // We found the parent node, add the new trace as a child + _ if self.arena[entry].trace.depth == new_trace.depth - 1 => { + let id = self.arena.len(); + + let trace_location = self.arena[entry].children.len(); + self.arena[entry].ordering.push(LogCallOrder::Call(trace_location)); + let node = CallTraceNode { + parent: Some(entry), + trace: new_trace, + idx: id, + ..Default::default() + }; + self.arena.push(node); + self.arena[entry].children.push(id); + + id + } + // We haven't found the parent node, go deeper + _ => self.push_trace( + *self.arena[entry].children.last().expect("Disconnected trace"), + new_trace, + ), + } + } + + /// Returns the traces of the transaction for `trace_transaction` + pub fn parity_traces(&self) -> Vec { + let traces = Vec::with_capacity(self.arena.len()); + for (_idx, node) in self.arena.iter().cloned().enumerate() { + let _action = node.parity_action(); + let _result = node.parity_result(); + + let _action_type = if node.status() == InstructionResult::SelfDestruct { + ActionType::Selfdestruct + } else { + node.kind().into() + }; + + todo!() + + // let trace = TransactionTrace { + // action, + // result: Some(result), + // trace_address: self.info.trace_address(idx), + // subtraces: node.children.len(), + // }; + // traces.push(trace) + } + + traces + } + + /// Recursively fill in the geth trace by going through the traces + /// + /// TODO rewrite this iteratively + fn add_to_geth_trace( + &self, + storage: &mut HashMap>, + trace_node: &CallTraceNode, + struct_logs: &mut Vec, + opts: &GethDebugTracingOptions, + ) { + let mut child_id = 0; + // Iterate over the steps inside the given trace + for step in trace_node.trace.steps.iter() { + let mut log: StructLog = step.into(); + + // Fill in memory and storage depending on the options + if !opts.disable_storage.unwrap_or_default() { + let contract_storage = storage.entry(step.contract).or_default(); + if let Some((key, value)) = step.state_diff { + contract_storage.insert(key.into(), value.into()); + log.storage = Some(contract_storage.clone()); + } + } + if opts.disable_stack.unwrap_or_default() { + log.stack = None; + } + if !opts.enable_memory.unwrap_or_default() { + log.memory = None; + } + + // Add step to geth trace + struct_logs.push(log); + + // If the opcode is a call, the descend into child trace + match step.op.u8() { + opcode::CREATE | + opcode::CREATE2 | + opcode::DELEGATECALL | + opcode::CALL | + opcode::STATICCALL | + opcode::CALLCODE => { + self.add_to_geth_trace( + storage, + &self.arena[trace_node.children[child_id]], + struct_logs, + opts, + ); + child_id += 1; + } + _ => {} + } + } + } + + /// Generate a geth-style trace e.g. for `debug_traceTransaction` + pub fn geth_traces( + &self, + // TODO(mattsse): This should be the total gas used, or gas used by last CallTrace? + receipt_gas_used: U256, + opts: GethDebugTracingOptions, + ) -> DefaultFrame { + if self.arena.is_empty() { + return Default::default() + } + // Fetch top-level trace + let main_trace_node = &self.arena[0]; + let main_trace = &main_trace_node.trace; + + let mut struct_logs = Vec::new(); + let mut storage = HashMap::new(); + self.add_to_geth_trace(&mut storage, main_trace_node, &mut struct_logs, &opts); + + DefaultFrame { + // If the top-level trace succeeded, then it was a success + failed: !main_trace.success, + gas: JsonU256(receipt_gas_used), + return_value: main_trace.output.clone().into(), + struct_logs, + } + } +} diff --git a/crates/revm/revm-inspectors/src/tracing/mod.rs b/crates/revm/revm-inspectors/src/tracing/mod.rs new file mode 100644 index 0000000000..e5285a3e6b --- /dev/null +++ b/crates/revm/revm-inspectors/src/tracing/mod.rs @@ -0,0 +1,408 @@ +use crate::{ + stack::MaybeOwnedInspector, + tracing::{ + types::{CallKind, LogCallOrder, RawLog}, + utils::{gas_used, get_create_address}, + }, +}; +pub use arena::CallTraceArena; +use reth_primitives::{bytes::Bytes, Address, H256, U256}; +use revm::{ + inspectors::GasInspector, + interpreter::{ + opcode, return_ok, CallInputs, CallScheme, CreateInputs, Gas, InstructionResult, + Interpreter, OpCode, + }, + Database, EVMData, Inspector, JournalEntry, +}; +use types::{CallTrace, CallTraceStep}; + +mod arena; +mod types; +mod utils; + +/// An inspector that collects call traces. +/// +/// This [Inspector] can be hooked into the [EVM](revm::EVM) which then calls the inspector +/// functions, such as [Inspector::call] or [Inspector::call_end]. +/// +/// The [TracingInspector] keeps track of everything by: +/// 1. start tracking steps/calls on [Inspector::step] and [Inspector::call] +/// 2. complete steps/calls on [Inspector::step_end] and [Inspector::call_end] +#[derive(Default, Debug, Clone)] +pub struct TracingInspector { + /// Whether to include individual steps [Inspector::step] + record_steps: bool, + /// Records all call traces + traces: CallTraceArena, + trace_stack: Vec, + step_stack: Vec, + /// The gas inspector used to track remaining gas. + /// + /// This is either owned by this inspector directly or part of a stack of inspectors, in which + /// case all delegated functions are no-ops. + gas_inspector: MaybeOwnedInspector, +} + +// === impl TracingInspector === + +impl TracingInspector { + /// Consumes the Inspector and returns the recorded. + pub fn finalize(self) -> CallTraceArena { + self.traces + } + + /// Enables step recording and uses the configured [GasInspector] to report gas costs for each + /// step. + pub fn with_steps_recording(mut self) -> Self { + self.record_steps = true; + self + } + + /// Configures a [GasInspector] + /// + /// If this [TracingInspector] is part of a stack [InspectorStack](crate::stack::InspectorStack) + /// which already uses a [GasInspector], it can be reused as [MaybeOwnedInspector::Stacked] in + /// which case the `gas_inspector`'s usage will be a no-op within the context of this + /// [TracingInspector]. + pub fn with_stacked_gas_inspector( + mut self, + gas_inspector: MaybeOwnedInspector, + ) -> Self { + self.gas_inspector = gas_inspector; + self + } + + /// Returns the last trace [CallTrace] index from the stack. + /// + /// # Panics + /// + /// If no [CallTrace] was pushed + #[track_caller] + #[inline] + fn last_trace_idx(&self) -> usize { + self.trace_stack.last().copied().expect("can't start step without starting a trace first") + } + + /// _Removes_ the last trace [CallTrace] index from the stack. + /// + /// # Panics + /// + /// If no [CallTrace] was pushed + #[track_caller] + #[inline] + fn pop_trace_idx(&mut self) -> usize { + self.trace_stack.pop().expect("more traces were filled than started") + } + + /// Starts tracking a new trace. + /// + /// Invoked on [Inspector::call]. + fn start_trace_on_call( + &mut self, + depth: usize, + address: Address, + data: Bytes, + value: U256, + kind: CallKind, + caller: Address, + ) { + self.trace_stack.push(self.traces.push_trace( + 0, + CallTrace { + depth, + address, + kind, + data, + value, + status: InstructionResult::Continue, + caller, + ..Default::default() + }, + )); + } + + /// Fills the current trace with the outcome of a call. + /// + /// Invoked on [Inspector::call_end]. + /// + /// # Panics + /// + /// This expects an existing trace [Self::start_trace_on_call] + fn fill_trace_on_call_end( + &mut self, + status: InstructionResult, + gas_used: u64, + output: Bytes, + created_address: Option
, + ) { + let trace_idx = self.pop_trace_idx(); + let trace = &mut self.traces.arena[trace_idx].trace; + + let success = matches!(status, return_ok!()); + trace.status = status; + trace.success = success; + trace.gas_used = gas_used; + trace.output = output; + + if let Some(address) = created_address { + // A new contract was created via CREATE + trace.address = address; + } + } + + /// Starts tracking a step + /// + /// Invoked on [Inspector::step] + /// + /// # Panics + /// + /// This expects an existing [CallTrace], in other words, this panics if not within the context + /// of a call. + fn start_step(&mut self, interp: &mut Interpreter, data: &mut EVMData<'_, DB>) { + let trace_idx = self.last_trace_idx(); + let trace = &mut self.traces.arena[trace_idx]; + + self.step_stack.push(StackStep { trace_idx, step_idx: trace.trace.steps.len() }); + + let pc = interp.program_counter(); + + trace.trace.steps.push(CallTraceStep { + depth: data.journaled_state.depth(), + pc, + op: OpCode::try_from_u8(interp.contract.bytecode.bytecode()[pc]) + .expect("is valid opcode;"), + contract: interp.contract.address, + stack: interp.stack.clone(), + memory: interp.memory.clone(), + gas: self.gas_inspector.as_ref().gas_remaining(), + gas_refund_counter: interp.gas.refunded() as u64, + + // fields will be populated end of call + gas_cost: 0, + state_diff: None, + status: InstructionResult::Continue, + }); + } + + /// Fills the current trace with the output of a step. + /// + /// Invoked on [Inspector::step_end]. + fn fill_step_on_step_end( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + status: InstructionResult, + ) { + let StackStep { trace_idx, step_idx } = + self.step_stack.pop().expect("can't fill step without starting a step first"); + let step = &mut self.traces.arena[trace_idx].trace.steps[step_idx]; + + if let Some(pc) = interp.program_counter().checked_sub(1) { + let op = interp.contract.bytecode.bytecode()[pc]; + + let journal_entry = data + .journaled_state + .journal + .last() + // This should always work because revm initializes it as `vec![vec![]]` + // See [JournaledState::new](revm::JournaledState) + .expect("exists; initialized with vec") + .last(); + + step.state_diff = match (op, journal_entry) { + ( + opcode::SLOAD | opcode::SSTORE, + Some(JournalEntry::StorageChange { address, key, .. }), + ) => { + // SAFETY: (Address,key) exists if part if StorageChange + let value = data.journaled_state.state[address].storage[key].present_value(); + Some((*key, value)) + } + _ => None, + }; + + step.gas_cost = step.gas - self.gas_inspector.as_ref().gas_remaining(); + } + + // set the status + step.status = status; + } +} + +impl Inspector for TracingInspector +where + DB: Database, +{ + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + ) -> InstructionResult { + self.gas_inspector.initialize_interp(interp, data, is_static) + } + + fn step( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + ) -> InstructionResult { + if self.record_steps { + self.gas_inspector.step(interp, data, is_static); + self.start_step(interp, data); + } + + InstructionResult::Continue + } + + fn log( + &mut self, + evm_data: &mut EVMData<'_, DB>, + address: &Address, + topics: &[H256], + data: &Bytes, + ) { + self.gas_inspector.log(evm_data, address, topics, data); + + let trace_idx = self.last_trace_idx(); + let trace = &mut self.traces.arena[trace_idx]; + trace.ordering.push(LogCallOrder::Log(trace.logs.len())); + trace.logs.push(RawLog { topics: topics.to_vec(), data: data.clone() }); + } + + fn step_end( + &mut self, + interp: &mut Interpreter, + data: &mut EVMData<'_, DB>, + is_static: bool, + eval: InstructionResult, + ) -> InstructionResult { + if self.record_steps { + self.gas_inspector.step_end(interp, data, is_static, eval); + self.fill_step_on_step_end(interp, data, eval); + return eval + } + InstructionResult::Continue + } + + fn call( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CallInputs, + is_static: bool, + ) -> (InstructionResult, Gas, Bytes) { + self.gas_inspector.call(data, inputs, is_static); + + // determine correct `from` and `to` based on the call scheme + let (from, to) = match inputs.context.scheme { + CallScheme::DelegateCall | CallScheme::CallCode => { + (inputs.context.address, inputs.context.code_address) + } + _ => (inputs.context.caller, inputs.context.address), + }; + + self.start_trace_on_call( + data.journaled_state.depth() as usize, + to, + inputs.input.clone(), + inputs.transfer.value, + inputs.context.scheme.into(), + from, + ); + + (InstructionResult::Continue, Gas::new(0), Bytes::new()) + } + + fn call_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CallInputs, + gas: Gas, + ret: InstructionResult, + out: Bytes, + is_static: bool, + ) -> (InstructionResult, Gas, Bytes) { + self.gas_inspector.call_end(data, inputs, gas, ret, out.clone(), is_static); + + self.fill_trace_on_call_end( + ret, + gas_used(data.env.cfg.spec_id, gas.spend(), gas.refunded() as u64), + out.clone(), + None, + ); + + (ret, gas, out) + } + + fn create( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &mut CreateInputs, + ) -> (InstructionResult, Option
, Gas, Bytes) { + self.gas_inspector.create(data, inputs); + + let _ = data.journaled_state.load_account(inputs.caller, data.db); + let nonce = data.journaled_state.account(inputs.caller).info.nonce; + self.start_trace_on_call( + data.journaled_state.depth() as usize, + get_create_address(inputs, nonce), + inputs.init_code.clone(), + inputs.value, + inputs.scheme.into(), + inputs.caller, + ); + + (InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::default()) + } + + /// Called when a contract has been created. + /// + /// InstructionResulting anything other than the values passed to this function (`(ret, + /// remaining_gas, address, out)`) will alter the result of the create. + fn create_end( + &mut self, + data: &mut EVMData<'_, DB>, + inputs: &CreateInputs, + status: InstructionResult, + address: Option
, + gas: Gas, + retdata: Bytes, + ) -> (InstructionResult, Option
, Gas, Bytes) { + self.gas_inspector.create_end(data, inputs, status, address, gas, retdata.clone()); + + // get the code of the created contract + let code = address + .and_then(|address| { + data.journaled_state + .account(address) + .info + .code + .as_ref() + .map(|code| code.bytes()[..code.len()].to_vec()) + }) + .unwrap_or_default(); + + self.fill_trace_on_call_end( + status, + gas_used(data.env.cfg.spec_id, gas.spend(), gas.refunded() as u64), + code.into(), + address, + ); + + (status, address, gas, retdata) + } + + fn selfdestruct(&mut self, _contract: Address, target: Address) { + let trace_idx = self.last_trace_idx(); + let trace = &mut self.traces.arena[trace_idx].trace; + trace.selfdestruct_refund_target = Some(target) + } +} + +#[derive(Debug, Clone, Copy)] +struct StackStep { + trace_idx: usize, + step_idx: usize, +} diff --git a/crates/revm/revm-inspectors/src/tracing/types.rs b/crates/revm/revm-inspectors/src/tracing/types.rs new file mode 100644 index 0000000000..3c13d29135 --- /dev/null +++ b/crates/revm/revm-inspectors/src/tracing/types.rs @@ -0,0 +1,293 @@ +//! Types for representing call trace items. + +use crate::tracing::utils::convert_memory; +use reth_primitives::{bytes::Bytes, Address, H256, U256}; +use reth_rpc_types::trace::{ + geth::StructLog, + parity::{ + Action, ActionType, CallAction, CallOutput, CallType, CreateAction, CreateOutput, + SelfdestructAction, TraceOutput, + }, +}; +use revm::interpreter::{ + CallContext, CallScheme, CreateScheme, InstructionResult, Memory, OpCode, Stack, +}; +use serde::{Deserialize, Serialize}; + +/// A unified representation of a call +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +#[allow(missing_docs)] +pub enum CallKind { + #[default] + Call, + StaticCall, + CallCode, + DelegateCall, + Create, + Create2, +} + +impl From for CallKind { + fn from(scheme: CallScheme) -> Self { + match scheme { + CallScheme::Call => CallKind::Call, + CallScheme::StaticCall => CallKind::StaticCall, + CallScheme::CallCode => CallKind::CallCode, + CallScheme::DelegateCall => CallKind::DelegateCall, + } + } +} + +impl From for CallKind { + fn from(create: CreateScheme) -> Self { + match create { + CreateScheme::Create => CallKind::Create, + CreateScheme::Create2 { .. } => CallKind::Create2, + } + } +} + +impl From for ActionType { + fn from(kind: CallKind) -> Self { + match kind { + CallKind::Call | CallKind::StaticCall | CallKind::DelegateCall | CallKind::CallCode => { + ActionType::Call + } + CallKind::Create => ActionType::Create, + CallKind::Create2 => ActionType::Create, + } + } +} + +impl From for CallType { + fn from(ty: CallKind) -> Self { + match ty { + CallKind::Call => CallType::Call, + CallKind::StaticCall => CallType::StaticCall, + CallKind::CallCode => CallType::CallCode, + CallKind::DelegateCall => CallType::DelegateCall, + CallKind::Create => CallType::None, + CallKind::Create2 => CallType::None, + } + } +} + +/// A trace of a call. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct CallTrace { + /// The depth of the call + pub(crate) depth: usize, + /// Whether the call was successful + pub(crate) success: bool, + /// caller of this call + pub(crate) caller: Address, + /// The destination address of the call or the address from the created contract. + /// + /// In other words, this is the callee if the [CallKind::Call] or the address of the created + /// contract if [CallKind::Create]. + pub(crate) address: Address, + /// Holds the target for the selfdestruct refund target if `status` is + /// [InstructionResult::SelfDestruct] + pub(crate) selfdestruct_refund_target: Option
, + /// The kind of call this is + pub(crate) kind: CallKind, + /// The value transferred in the call + pub(crate) value: U256, + /// The calldata for the call, or the init code for contract creations + pub(crate) data: Bytes, + /// The return data of the call if this was not a contract creation, otherwise it is the + /// runtime bytecode of the created contract + pub(crate) output: Bytes, + /// The gas cost of the call + pub(crate) gas_used: u64, + /// The status of the trace's call + pub(crate) status: InstructionResult, + /// call context of the runtime + pub(crate) call_context: Option, + /// Opcode-level execution steps + pub(crate) steps: Vec, +} + +impl Default for CallTrace { + fn default() -> Self { + Self { + depth: Default::default(), + success: Default::default(), + caller: Default::default(), + address: Default::default(), + selfdestruct_refund_target: None, + kind: Default::default(), + value: Default::default(), + data: Default::default(), + output: Default::default(), + gas_used: Default::default(), + status: InstructionResult::Continue, + call_context: Default::default(), + steps: Default::default(), + } + } +} + +/// A node in the arena +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub(crate) struct CallTraceNode { + /// Parent node index in the arena + pub(crate) parent: Option, + /// Children node indexes in the arena + pub(crate) children: Vec, + /// This node's index in the arena + pub(crate) idx: usize, + /// The call trace + pub(crate) trace: CallTrace, + /// Logs + pub(crate) logs: Vec, + /// Ordering of child calls and logs + pub(crate) ordering: Vec, +} + +impl CallTraceNode { + /// Returns the kind of call the trace belongs to + pub(crate) fn kind(&self) -> CallKind { + self.trace.kind + } + + /// Returns the status of the call + pub(crate) fn status(&self) -> InstructionResult { + self.trace.status + } + + /// Returns the `Output` for a parity trace + pub(crate) fn parity_result(&self) -> TraceOutput { + match self.kind() { + CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => { + TraceOutput::Call(CallOutput { + gas_used: self.trace.gas_used.into(), + output: self.trace.output.clone().into(), + }) + } + CallKind::Create | CallKind::Create2 => TraceOutput::Create(CreateOutput { + gas_used: self.trace.gas_used.into(), + code: self.trace.output.clone().into(), + address: self.trace.address, + }), + } + } + + /// Returns the `Action` for a parity trace + pub(crate) fn parity_action(&self) -> Action { + if self.status() == InstructionResult::SelfDestruct { + return Action::Selfdestruct(SelfdestructAction { + address: self.trace.address, + refund_address: self.trace.selfdestruct_refund_target.unwrap_or_default(), + balance: self.trace.value, + }) + } + match self.kind() { + CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => { + Action::Call(CallAction { + from: self.trace.caller, + to: self.trace.address, + value: self.trace.value, + gas: self.trace.gas_used.into(), + input: self.trace.data.clone().into(), + call_type: self.kind().into(), + }) + } + CallKind::Create | CallKind::Create2 => Action::Create(CreateAction { + from: self.trace.caller, + value: self.trace.value, + gas: self.trace.gas_used.into(), + init: self.trace.data.clone().into(), + }), + } + } +} + +/// Ordering enum for calls and logs +/// +/// i.e. if Call 0 occurs before Log 0, it will be pushed into the `CallTraceNode`'s ordering before +/// the log. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum LogCallOrder { + Log(usize), + Call(usize), +} + +/// Ethereum log. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RawLog { + /// Indexed event params are represented as log topics. + pub(crate) topics: Vec, + /// Others are just plain data. + pub(crate) data: Bytes, +} + +/// Represents a tracked call step during execution +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CallTraceStep { + // Fields filled in `step` + /// Call depth + pub depth: u64, + /// Program counter before step execution + pub pc: usize, + /// Opcode to be executed + pub op: OpCode, + /// Current contract address + pub contract: Address, + /// Stack before step execution + pub stack: Stack, + /// Memory before step execution + pub memory: Memory, + /// Remaining gas before step execution + pub gas: u64, + /// Gas refund counter before step execution + pub gas_refund_counter: u64, + // Fields filled in `step_end` + /// Gas cost of step execution + pub gas_cost: u64, + /// Change of the contract state after step execution (effect of the SLOAD/SSTORE instructions) + pub state_diff: Option<(U256, U256)>, + /// Final status of the call + pub status: InstructionResult, +} + +// === impl CallTraceStep === + +impl CallTraceStep { + // Returns true if the status code is an error or revert, See [InstructionResult::Revert] + pub fn is_error(&self) -> bool { + self.status as u8 >= InstructionResult::Revert as u8 + } + + /// Returns the error message if it is an erroneous result. + pub fn as_error(&self) -> Option { + if self.is_error() { + Some(format!("{:?}", self.status)) + } else { + None + } + } +} + +impl From<&CallTraceStep> for StructLog { + fn from(step: &CallTraceStep) -> Self { + StructLog { + depth: step.depth, + error: step.as_error(), + gas: step.gas, + gas_cost: step.gas_cost, + memory: Some(convert_memory(step.memory.data())), + op: step.op.to_string(), + pc: step.pc as u64, + refund_counter: if step.gas_refund_counter > 0 { + Some(step.gas_refund_counter) + } else { + None + }, + stack: Some(step.stack.data().clone()), + // Filled in `CallTraceArena::geth_trace` as a result of compounding all slot changes + storage: None, + } + } +} diff --git a/crates/revm/revm-inspectors/src/tracing/utils.rs b/crates/revm/revm-inspectors/src/tracing/utils.rs new file mode 100644 index 0000000000..64468f8250 --- /dev/null +++ b/crates/revm/revm-inspectors/src/tracing/utils.rs @@ -0,0 +1,40 @@ +//! Util functions for revm related ops + +use reth_primitives::{ + contract::{create2_address_from_code, create_address}, + hex, Address, +}; +use revm::{ + interpreter::CreateInputs, + primitives::{CreateScheme, SpecId}, +}; + +/// creates the memory data in 32byte chunks +/// see +#[inline] +pub(crate) fn convert_memory(data: &[u8]) -> Vec { + let mut memory = Vec::with_capacity((data.len() + 31) / 32); + for idx in (0..data.len()).step_by(32) { + let len = std::cmp::min(idx + 32, data.len()); + memory.push(hex::encode(&data[idx..len])); + } + memory +} + +/// Get the gas used, accounting for refunds +#[inline] +pub(crate) fn gas_used(spec: SpecId, spent: u64, refunded: u64) -> u64 { + let refund_quotient = if SpecId::enabled(spec, SpecId::LONDON) { 5 } else { 2 }; + spent - (refunded).min(spent / refund_quotient) +} + +/// Get the address of a contract creation +#[inline] +pub(crate) fn get_create_address(call: &CreateInputs, nonce: u64) -> Address { + match call.scheme { + CreateScheme::Create => create_address(call.caller, nonce), + CreateScheme::Create2 { salt } => { + create2_address_from_code(call.caller, call.init_code.clone(), salt) + } + } +} diff --git a/crates/rpc/rpc-types/Cargo.toml b/crates/rpc/rpc-types/Cargo.toml index dcf39ef795..e1abd62a9f 100644 --- a/crates/rpc/rpc-types/Cargo.toml +++ b/crates/rpc/rpc-types/Cargo.toml @@ -14,6 +14,9 @@ reth-primitives = { path = "../../primitives" } reth-rlp = { path = "../../rlp" } reth-network-api = { path = "../../net/network-api"} +# for geth tracing types +ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } + # errors thiserror = "1.0" diff --git a/crates/rpc/rpc-types/src/eth/trace/mod.rs b/crates/rpc/rpc-types/src/eth/trace/mod.rs index 9270dbe1e5..cc07e21cb3 100644 --- a/crates/rpc/rpc-types/src/eth/trace/mod.rs +++ b/crates/rpc/rpc-types/src/eth/trace/mod.rs @@ -2,3 +2,51 @@ pub mod filter; pub mod parity; + +/// Geth tracing types +pub mod geth { + #![allow(missing_docs)] + + use reth_primitives::{Bytes, JsonU256, H256, U256}; + use serde::{Deserialize, Serialize}; + use std::collections::BTreeMap; + + // re-exported for geth tracing types + pub use ethers_core::types::GethDebugTracingOptions; + + /// Geth Default trace frame + /// + /// + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct DefaultFrame { + pub failed: bool, + pub gas: JsonU256, + pub return_value: Bytes, + pub struct_logs: Vec, + } + + /// Represents a struct log entry in a trace + /// + /// + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] + pub struct StructLog { + pub depth: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + pub gas: u64, + #[serde(rename = "gasCost")] + pub gas_cost: u64, + /// ref + #[serde(default, skip_serializing_if = "Option::is_none")] + pub memory: Option>, + pub op: String, + pub pc: u64, + #[serde(default, rename = "refund", skip_serializing_if = "Option::is_none")] + pub refund_counter: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stack: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub storage: Option>, + } +} diff --git a/crates/rpc/rpc-types/src/eth/trace/parity.rs b/crates/rpc/rpc-types/src/eth/trace/parity.rs index 80c4359794..174f1f13d7 100644 --- a/crates/rpc/rpc-types/src/eth/trace/parity.rs +++ b/crates/rpc/rpc-types/src/eth/trace/parity.rs @@ -75,11 +75,26 @@ pub enum Action { Reward(RewardAction), } +/// An external action type. +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ActionType { + /// Contract call. + Call, + /// Contract creation. + Create, + /// Contract suicide/selfdestruct. + Selfdestruct, + /// A block reward. + Reward, +} + /// Call type. -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CallType { /// None + #[default] None, /// Call Call,