mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat: add Tracing inspector (#1618)
This commit is contained in:
@ -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"
|
||||
hashbrown = "0.13"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@ -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;
|
||||
|
||||
209
crates/revm/revm-inspectors/src/stack/maybe_owned.rs
Normal file
209
crates/revm/revm-inspectors/src/stack/maybe_owned.rs
Normal file
@ -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<INSP> {
|
||||
/// Inspector is owned.
|
||||
Owned(Rc<RefCell<INSP>>),
|
||||
/// Inspector is shared and part of a stack
|
||||
Stacked(Rc<RefCell<INSP>>),
|
||||
}
|
||||
|
||||
impl<INSP> MaybeOwnedInspector<INSP> {
|
||||
/// 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<INSP: Default> MaybeOwnedInspector<INSP> {
|
||||
/// Create a new _owned_ instance
|
||||
pub fn owned() -> Self {
|
||||
Self::new_owned(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<INSP: Default> Default for MaybeOwnedInspector<INSP> {
|
||||
fn default() -> Self {
|
||||
Self::owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl<INSP> Clone for MaybeOwnedInspector<INSP> {
|
||||
fn clone(&self) -> Self {
|
||||
self.clone_stacked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<INSP, DB> Inspector<DB> for MaybeOwnedInspector<INSP>
|
||||
where
|
||||
DB: Database,
|
||||
INSP: Inspector<DB>,
|
||||
{
|
||||
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<B160>, 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<B160>,
|
||||
remaining_gas: Gas,
|
||||
out: Bytes,
|
||||
) -> (InstructionResult, Option<B160>, 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(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
160
crates/revm/revm-inspectors/src/tracing/arena.rs
Normal file
160
crates/revm/revm-inspectors/src/tracing/arena.rs
Normal file
@ -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<CallTraceNode>,
|
||||
}
|
||||
|
||||
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<TransactionTrace> {
|
||||
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<Address, BTreeMap<H256, H256>>,
|
||||
trace_node: &CallTraceNode,
|
||||
struct_logs: &mut Vec<StructLog>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
408
crates/revm/revm-inspectors/src/tracing/mod.rs
Normal file
408
crates/revm/revm-inspectors/src/tracing/mod.rs
Normal file
@ -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<usize>,
|
||||
step_stack: Vec<StackStep>,
|
||||
/// 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<GasInspector>,
|
||||
}
|
||||
|
||||
// === 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<GasInspector>,
|
||||
) -> 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<Address>,
|
||||
) {
|
||||
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<DB: Database>(&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<DB: Database>(
|
||||
&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<DB> Inspector<DB> 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<Address>, 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<Address>,
|
||||
gas: Gas,
|
||||
retdata: Bytes,
|
||||
) -> (InstructionResult, Option<Address>, 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,
|
||||
}
|
||||
293
crates/revm/revm-inspectors/src/tracing/types.rs
Normal file
293
crates/revm/revm-inspectors/src/tracing/types.rs
Normal file
@ -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<CallScheme> 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<CreateScheme> for CallKind {
|
||||
fn from(create: CreateScheme) -> Self {
|
||||
match create {
|
||||
CreateScheme::Create => CallKind::Create,
|
||||
CreateScheme::Create2 { .. } => CallKind::Create2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallKind> 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<CallKind> 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<Address>,
|
||||
/// 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<CallContext>,
|
||||
/// Opcode-level execution steps
|
||||
pub(crate) steps: Vec<CallTraceStep>,
|
||||
}
|
||||
|
||||
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<usize>,
|
||||
/// Children node indexes in the arena
|
||||
pub(crate) children: Vec<usize>,
|
||||
/// This node's index in the arena
|
||||
pub(crate) idx: usize,
|
||||
/// The call trace
|
||||
pub(crate) trace: CallTrace,
|
||||
/// Logs
|
||||
pub(crate) logs: Vec<RawLog>,
|
||||
/// Ordering of child calls and logs
|
||||
pub(crate) ordering: Vec<LogCallOrder>,
|
||||
}
|
||||
|
||||
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<H256>,
|
||||
/// 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<String> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
40
crates/revm/revm-inspectors/src/tracing/utils.rs
Normal file
40
crates/revm/revm-inspectors/src/tracing/utils.rs
Normal file
@ -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 <https://github.com/ethereum/go-ethereum/blob/366d2169fbc0e0f803b68c042b77b6b480836dbc/eth/tracers/logger/logger.go#L450-L452>
|
||||
#[inline]
|
||||
pub(crate) fn convert_memory(data: &[u8]) -> Vec<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user