feat: add Tracing inspector (#1618)

This commit is contained in:
Matthias Seitz
2023-03-09 22:43:24 +01:00
committed by GitHub
parent ea11461126
commit 6190eac9ce
12 changed files with 1191 additions and 2 deletions

View File

@ -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"] }

View File

@ -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;

View 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(_) => {}
}
}
}

View File

@ -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

View 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,
}
}
}

View 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,
}

View 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,
}
}
}

View 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)
}
}
}