From f0cf93e0f9f8ef675d2d5cd0e892c4d6fea2112b Mon Sep 17 00:00:00 2001 From: N Date: Wed, 12 Jul 2023 14:35:00 -0400 Subject: [PATCH] feat: complete vm and statediff tracers (#3529) Co-authored-by: N Co-authored-by: Matthias Seitz --- .../src/tracing/builder/mod.rs | 3 + .../src/tracing/builder/parity.rs | 173 ++++++++++++++++-- .../src/tracing/builder/walker.rs | 39 ++++ .../revm/revm-inspectors/src/tracing/mod.rs | 5 + .../revm/revm-inspectors/src/tracing/types.rs | 4 +- 5 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 crates/revm/revm-inspectors/src/tracing/builder/walker.rs diff --git a/crates/revm/revm-inspectors/src/tracing/builder/mod.rs b/crates/revm/revm-inspectors/src/tracing/builder/mod.rs index 677ae88da5..e6e58d8c2d 100644 --- a/crates/revm/revm-inspectors/src/tracing/builder/mod.rs +++ b/crates/revm/revm-inspectors/src/tracing/builder/mod.rs @@ -5,3 +5,6 @@ pub mod geth; /// Parity style trace builders for `trace_` namespace pub mod parity; + +/// Walker types used for traversing various callgraphs +mod walker; diff --git a/crates/revm/revm-inspectors/src/tracing/builder/parity.rs b/crates/revm/revm-inspectors/src/tracing/builder/parity.rs index 3798d19f86..ba73f6cef0 100644 --- a/crates/revm/revm-inspectors/src/tracing/builder/parity.rs +++ b/crates/revm/revm-inspectors/src/tracing/builder/parity.rs @@ -1,11 +1,16 @@ -use crate::tracing::{types::CallTraceNode, TracingInspectorConfig}; +use super::walker::CallTraceNodeWalkerBF; +use crate::tracing::{ + types::{CallTraceNode, CallTraceStep}, + TracingInspectorConfig, +}; use reth_primitives::{Address, U64}; use reth_rpc_types::{trace::parity::*, TransactionInfo}; use revm::{ db::DatabaseRef, - primitives::{AccountInfo, ExecutionResult, ResultAndState}, + interpreter::opcode, + primitives::{AccountInfo, ExecutionResult, ResultAndState, KECCAK_EMPTY}, }; -use std::collections::HashSet; +use std::collections::{HashSet, VecDeque}; /// A type for creating parity style traces /// @@ -14,6 +19,7 @@ use std::collections::HashSet; pub struct ParityTraceBuilder { /// Recorded trace nodes nodes: Vec, + /// How the traces were recorded _config: TracingInspectorConfig, } @@ -154,7 +160,18 @@ impl ParityTraceBuilder { DB: DatabaseRef, { let ResultAndState { result, state } = res; + + let breadth_first_addresses = if trace_types.contains(&TraceType::VmTrace) { + CallTraceNodeWalkerBF::new(&self.nodes) + .map(|node| node.trace.address) + .collect::>() + } else { + vec![] + }; + let mut trace_res = self.into_trace_results(result, trace_types); + + // check the state diff case if let Some(ref mut state_diff) = trace_res.state_diff { populate_account_balance_nonce_diffs( state_diff, @@ -162,6 +179,12 @@ impl ParityTraceBuilder { state.into_iter().map(|(addr, acc)| (addr, acc.info)), )?; } + + // check the vm trace case + if let Some(ref mut vm_trace) = trace_res.vm_trace { + populate_vm_trace_bytecodes(&db, vm_trace, breadth_first_addresses)?; + } + Ok(trace_res) } @@ -177,11 +200,8 @@ impl ParityTraceBuilder { let with_traces = trace_types.contains(&TraceType::Trace); let with_diff = trace_types.contains(&TraceType::StateDiff); - let vm_trace = if trace_types.contains(&TraceType::VmTrace) { - Some(vm_trace(&self.nodes)) - } else { - None - }; + let vm_trace = + if trace_types.contains(&TraceType::VmTrace) { Some(self.vm_trace()) } else { None }; let mut traces = Vec::with_capacity(if with_traces { self.nodes.len() } else { 0 }); let mut diff = StateDiff::default(); @@ -218,13 +238,142 @@ impl ParityTraceBuilder { pub fn into_transaction_traces(self) -> Vec { self.into_transaction_traces_iter().collect() } + + /// Creates a VM trace by walking over `CallTraceNode`s + /// + /// does not have the code fields filled in + pub fn vm_trace(&self) -> VmTrace { + match self.nodes.get(0) { + Some(current) => self.make_vm_trace(current), + None => VmTrace { code: Default::default(), ops: Vec::new() }, + } + } + + /// returns a VM trace without the code filled in + /// + /// iteratively creaters a VM trace by traversing an arena + fn make_vm_trace(&self, start: &CallTraceNode) -> VmTrace { + let mut child_idx_stack: Vec = Vec::with_capacity(self.nodes.len()); + let mut sub_stack: VecDeque> = VecDeque::with_capacity(self.nodes.len()); + + let mut current = start; + let mut child_idx: usize = 0; + + // finds the deepest nested calls of each call frame and fills them up bottom to top + let instructions = loop { + match current.children.get(child_idx) { + Some(child) => { + child_idx_stack.push(child_idx + 1); + + child_idx = 0; + current = self.nodes.get(*child).expect("there should be a child"); + } + None => { + let mut instructions: Vec = + Vec::with_capacity(current.trace.steps.len()); + + for step in ¤t.trace.steps { + let maybe_sub = match step.op.u8() { + opcode::CALL | + opcode::CALLCODE | + opcode::DELEGATECALL | + opcode::STATICCALL | + opcode::CREATE | + opcode::CREATE2 => { + sub_stack.pop_front().expect("there should be a sub trace") + } + _ => None, + }; + + instructions.push(Self::make_instruction(step, maybe_sub)); + } + + match current.parent { + Some(parent) => { + sub_stack.push_back(Some(VmTrace { + code: Default::default(), + ops: instructions, + })); + + child_idx = child_idx_stack.pop().expect("there should be a child idx"); + + current = self.nodes.get(parent).expect("there should be a parent"); + } + None => break instructions, + } + } + } + }; + + VmTrace { code: Default::default(), ops: instructions } + } + + /// Creates a VM instruction from a [CallTraceStep] and a [VmTrace] for the subcall if there is + /// one + fn make_instruction(step: &CallTraceStep, maybe_sub: Option) -> VmInstruction { + let maybe_storage = step.storage_change.map(|storage_change| StorageDelta { + key: storage_change.key, + val: storage_change.value, + }); + + let maybe_memory = match step.memory.len() { + 0 => None, + _ => { + Some(MemoryDelta { off: step.memory_size, data: step.memory.data().clone().into() }) + } + }; + + let maybe_execution = Some(VmExecutedOperation { + used: step.gas_cost, + push: step.new_stack.map(|new_stack| new_stack.into()), + mem: maybe_memory, + store: maybe_storage, + }); + + VmInstruction { + pc: step.pc, + cost: 0, // TODO: use op gas cost + ex: maybe_execution, + sub: maybe_sub, + } + } } -/// Construct the vmtrace for the entire callgraph -fn vm_trace(nodes: &[CallTraceNode]) -> VmTrace { - // TODO: populate vm trace +/// addresses are presorted via breadth first walk thru [CallTraceNode]s, this can be done by a +/// walker in [crate::tracing::builder::walker] +/// +/// iteratively fill the [VmTrace] code fields +pub(crate) fn populate_vm_trace_bytecodes( + db: &DB, + trace: &mut VmTrace, + breadth_first_addresses: I, +) -> Result<(), DB::Error> +where + DB: DatabaseRef, + I: IntoIterator, +{ + let mut stack: VecDeque<&mut VmTrace> = VecDeque::new(); + stack.push_back(trace); - VmTrace { code: nodes[0].trace.data.clone().into(), ops: vec![] } + let mut addrs = breadth_first_addresses.into_iter(); + + while let Some(curr_ref) = stack.pop_front() { + for op in curr_ref.ops.iter_mut() { + if let Some(sub) = op.sub.as_mut() { + stack.push_back(sub); + } + } + + let addr = addrs.next().expect("there should be an address"); + + let db_acc = db.basic(addr)?.unwrap_or_default(); + + let code_hash = if db_acc.code_hash != KECCAK_EMPTY { db_acc.code_hash } else { continue }; + + curr_ref.code = db.code_by_hash(code_hash)?.bytecode.into(); + } + + Ok(()) } /// Loops over all state accounts in the accounts diff that contains all accounts that are included diff --git a/crates/revm/revm-inspectors/src/tracing/builder/walker.rs b/crates/revm/revm-inspectors/src/tracing/builder/walker.rs new file mode 100644 index 0000000000..4d88a2af46 --- /dev/null +++ b/crates/revm/revm-inspectors/src/tracing/builder/walker.rs @@ -0,0 +1,39 @@ +use crate::tracing::types::CallTraceNode; +use std::collections::VecDeque; + +/// Traverses Reths internal tracing structure breadth-first +/// +/// This is a lazy iterator +pub(crate) struct CallTraceNodeWalkerBF<'trace> { + /// the entire arena + nodes: &'trace Vec, + + /// holds indexes of nodes to visit as we traverse + queue: VecDeque, +} + +impl<'trace> CallTraceNodeWalkerBF<'trace> { + pub(crate) fn new(nodes: &'trace Vec) -> Self { + let mut queue = VecDeque::with_capacity(nodes.len()); + queue.push_back(0); + + Self { nodes, queue } + } +} + +impl<'trace> Iterator for CallTraceNodeWalkerBF<'trace> { + type Item = &'trace CallTraceNode; + + fn next(&mut self) -> Option { + match self.queue.pop_front() { + Some(idx) => { + let curr = self.nodes.get(idx).expect("there should be a node"); + + self.queue.extend(curr.children.iter()); + + Some(curr) + } + None => None, + } + } +} diff --git a/crates/revm/revm-inspectors/src/tracing/mod.rs b/crates/revm/revm-inspectors/src/tracing/mod.rs index bf3e746c90..53a26f765a 100644 --- a/crates/revm/revm-inspectors/src/tracing/mod.rs +++ b/crates/revm/revm-inspectors/src/tracing/mod.rs @@ -265,6 +265,7 @@ impl TracingInspector { op, contract: interp.contract.address, stack, + new_stack: None, memory, memory_size: interp.memory.len(), gas_remaining: self.gas_inspector.gas_remaining(), @@ -290,6 +291,10 @@ impl TracingInspector { 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 interp.stack.len() > step.stack.len() { + step.new_stack = interp.stack.data().last().copied(); + } + if self.config.record_memory_snapshots { // resize memory so opcodes that allocated memory is correctly displayed if interp.memory.len() > step.memory.len() { diff --git a/crates/revm/revm-inspectors/src/tracing/types.rs b/crates/revm/revm-inspectors/src/tracing/types.rs index b66bd67261..548ae72b87 100644 --- a/crates/revm/revm-inspectors/src/tracing/types.rs +++ b/crates/revm/revm-inspectors/src/tracing/types.rs @@ -463,11 +463,13 @@ pub(crate) struct CallTraceStep { pub(crate) contract: Address, /// Stack before step execution pub(crate) stack: Stack, + /// The new stack item placed by this step if any + pub(crate) new_stack: Option, /// All allocated memory in a step /// /// This will be empty if memory capture is disabled pub(crate) memory: Memory, - /// Size of memory + /// Size of memory at the beginning of the step pub(crate) memory_size: usize, /// Remaining gas before step execution pub(crate) gas_remaining: u64,