Files
nanoreth/crates/revm/revm-inspectors/src/tracing/builder/parity.rs
2023-10-25 20:01:25 +00:00

644 lines
23 KiB
Rust

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,
interpreter::opcode::{self, spec_opcode_gas},
primitives::{Account, ExecutionResult, ResultAndState, SpecId, KECCAK_EMPTY},
};
use std::collections::{HashSet, VecDeque};
/// A type for creating parity style traces
///
/// Note: Parity style traces always ignore calls to precompiles.
#[derive(Clone, Debug)]
pub struct ParityTraceBuilder {
/// Recorded trace nodes
nodes: Vec<CallTraceNode>,
/// The spec id of the EVM.
spec_id: Option<SpecId>,
/// How the traces were recorded
_config: TracingInspectorConfig,
}
impl ParityTraceBuilder {
/// Returns a new instance of the builder
pub(crate) fn new(
nodes: Vec<CallTraceNode>,
spec_id: Option<SpecId>,
_config: TracingInspectorConfig,
) -> Self {
Self { nodes, spec_id, _config }
}
/// Returns a list of all addresses that appeared as callers.
pub fn callers(&self) -> HashSet<Address> {
self.nodes.iter().map(|node| node.trace.caller).collect()
}
/// Manually the gas used of the root trace.
///
/// The root trace's gasUsed should mirror the actual gas used by the transaction.
///
/// This allows setting it manually by consuming the execution result's gas for example.
#[inline]
pub fn set_transaction_gas_used(&mut self, gas_used: u64) {
if let Some(node) = self.nodes.first_mut() {
node.trace.gas_used = gas_used;
}
}
/// Convenience function for [ParityTraceBuilder::set_transaction_gas_used] that consumes the
/// type.
#[inline]
pub fn with_transaction_gas_used(mut self, gas_used: u64) -> Self {
self.set_transaction_gas_used(gas_used);
self
}
/// Returns the trace addresses of all call nodes in the set
///
/// Each entry in the returned vector represents the [Self::trace_address] of the corresponding
/// node in the nodes set.
///
/// CAUTION: This also includes precompiles, which have an empty trace address.
fn trace_addresses(&self) -> Vec<Vec<usize>> {
let mut all_addresses = Vec::with_capacity(self.nodes.len());
for idx in 0..self.nodes.len() {
all_addresses.push(self.trace_address(idx));
}
all_addresses
}
/// Returns the `traceAddress` of the node in the arena
///
/// The `traceAddress` field of all returned traces, gives the exact location in the call trace
/// [index in root, index in first CALL, index in second CALL, …].
///
/// # Panics
///
/// if the `idx` does not belong to a node
///
/// Note: if the call node of `idx` is a precompile, the returned trace address will be empty.
fn trace_address(&self, idx: usize) -> Vec<usize> {
if idx == 0 {
// root call has empty traceAddress
return vec![]
}
let mut graph = vec![];
let mut node = &self.nodes[idx];
if node.is_precompile() {
return graph
}
while let Some(parent) = node.parent {
// the index of the child call in the arena
let child_idx = node.idx;
node = &self.nodes[parent];
// find the index of the child call in the parent node
let call_idx = node
.children
.iter()
.position(|child| *child == child_idx)
.expect("non precompile child call exists in parent");
graph.push(call_idx);
}
graph.reverse();
graph
}
/// Returns an iterator over all nodes to trace
///
/// This excludes nodes that represent calls to precompiles.
fn iter_traceable_nodes(&self) -> impl Iterator<Item = &CallTraceNode> {
self.nodes.iter().filter(|node| !node.is_precompile())
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_localized_transaction_traces_iter(
self,
info: TransactionInfo,
) -> impl Iterator<Item = LocalizedTransactionTrace> {
self.into_transaction_traces_iter().map(move |trace| {
let TransactionInfo { hash, index, block_hash, block_number, .. } = info;
LocalizedTransactionTrace {
trace,
transaction_position: index,
transaction_hash: hash,
block_number,
block_hash,
}
})
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_localized_transaction_traces(
self,
info: TransactionInfo,
) -> Vec<LocalizedTransactionTrace> {
self.into_localized_transaction_traces_iter(info).collect()
}
/// Consumes the inspector and returns the trace results according to the configured trace
/// types.
///
/// Warning: If `trace_types` contains [TraceType::StateDiff] the returned [StateDiff] will not
/// be filled. Use [ParityTraceBuilder::into_trace_results_with_state] or
/// [populate_state_diff] to populate the balance and nonce changes for the [StateDiff]
/// using the [DatabaseRef].
pub fn into_trace_results(
self,
res: &ExecutionResult,
trace_types: &HashSet<TraceType>,
) -> TraceResults {
let gas_used = res.gas_used();
let output = res.output().cloned().unwrap_or_default();
let (trace, vm_trace, state_diff) = self.into_trace_type_traces(trace_types);
let mut trace =
TraceResults { output, trace: trace.unwrap_or_default(), vm_trace, state_diff };
// we're setting the gas used of the root trace explicitly to the gas used of the execution
// result
trace.set_root_trace_gas_used(gas_used);
trace
}
/// Consumes the inspector and returns the trace results according to the configured trace
/// types.
///
/// This also takes the [DatabaseRef] to populate the balance and nonce changes for the
/// [StateDiff].
///
/// Note: this is considered a convenience method that takes the state map of
/// [ResultAndState] after inspecting a transaction
/// with the [TracingInspector](crate::tracing::TracingInspector).
pub fn into_trace_results_with_state<DB>(
self,
res: &ResultAndState,
trace_types: &HashSet<TraceType>,
db: DB,
) -> Result<TraceResults, DB::Error>
where
DB: DatabaseRef,
{
let ResultAndState { ref result, ref state } = res;
let breadth_first_addresses = if trace_types.contains(&TraceType::VmTrace) {
CallTraceNodeWalkerBF::new(&self.nodes)
.map(|node| node.trace.address)
.collect::<Vec<_>>()
} 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_state_diff(state_diff, &db, state.iter())?;
}
// 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)
}
/// Returns the tracing types that are configured in the set.
///
/// Warning: if [TraceType::StateDiff] is provided this does __not__ fill the state diff, since
/// this requires access to the account diffs.
///
/// See [Self::into_trace_results_with_state] and [populate_state_diff].
pub fn into_trace_type_traces(
self,
trace_types: &HashSet<TraceType>,
) -> (Option<Vec<TransactionTrace>>, Option<VmTrace>, Option<StateDiff>) {
if trace_types.is_empty() || self.nodes.is_empty() {
return (None, None, None)
}
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(self.vm_trace()) } else { None };
let mut traces = Vec::with_capacity(if with_traces { self.nodes.len() } else { 0 });
for node in self.iter_traceable_nodes() {
let trace_address = self.trace_address(node.idx);
if with_traces {
let trace = node.parity_transaction_trace(trace_address);
traces.push(trace);
// check if the trace node is a selfdestruct
if node.is_selfdestruct() {
// selfdestructs are not recorded as individual call traces but are derived from
// the call trace and are added as additional `TransactionTrace` objects in the
// trace array
let addr = {
let last = traces.last_mut().expect("exists");
let mut addr = last.trace_address.clone();
addr.push(last.subtraces);
// need to account for the additional selfdestruct trace
last.subtraces += 1;
addr
};
if let Some(trace) = node.parity_selfdestruct_trace(addr) {
traces.push(trace);
}
}
}
}
let traces = with_traces.then_some(traces);
let diff = with_diff.then_some(StateDiff::default());
(traces, vm_trace, diff)
}
/// Returns an iterator over all recorded traces for `trace_transaction`
pub fn into_transaction_traces_iter(self) -> impl Iterator<Item = TransactionTrace> {
let trace_addresses = self.trace_addresses();
TransactionTraceIter {
next_selfdestruct: None,
iter: self
.nodes
.into_iter()
.zip(trace_addresses)
.filter(|(node, _)| !node.is_precompile())
.map(|(node, trace_address)| (node.parity_transaction_trace(trace_address), node)),
}
}
/// Returns the raw traces of the transaction
pub fn into_transaction_traces(self) -> Vec<TransactionTrace> {
self.into_transaction_traces_iter().collect()
}
/// Returns the last recorded step
#[inline]
fn last_step(&self) -> Option<&CallTraceStep> {
self.nodes.last().and_then(|node| node.trace.steps.last())
}
/// Returns true if the last recorded step is a STOP
#[inline]
fn is_last_step_stop_op(&self) -> bool {
self.last_step().map(|step| step.is_stop()).unwrap_or(false)
}
/// Creates a VM trace by walking over `CallTraceNode`s
///
/// does not have the code fields filled in
pub fn vm_trace(&self) -> VmTrace {
self.nodes.first().map(|node| self.make_vm_trace(node)).unwrap_or_default()
}
/// Returns a VM trace without the code filled in
///
/// Iteratively creates a VM trace by traversing the recorded nodes in the arena
fn make_vm_trace(&self, start: &CallTraceNode) -> VmTrace {
let mut child_idx_stack = Vec::with_capacity(self.nodes.len());
let mut sub_stack = 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 = 'outer: 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::with_capacity(current.trace.steps.len());
for step in &current.trace.steps {
let maybe_sub_call = if step.is_calllike_op() {
sub_stack.pop_front().flatten()
} else {
None
};
if step.is_stop() && instructions.is_empty() && self.is_last_step_stop_op()
{
// This is a special case where there's a single STOP which is
// "optimised away", transfers for example
break 'outer instructions
}
instructions.push(self.make_instruction(step, maybe_sub_call));
}
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(
&self,
step: &CallTraceStep,
maybe_sub_call: Option<VmTrace>,
) -> VmInstruction {
let maybe_storage = step.storage_change.map(|storage_change| StorageDelta {
key: storage_change.key,
val: storage_change.value,
});
let maybe_memory = if step.memory.is_empty() {
None
} else {
Some(MemoryDelta {
off: step.memory_size,
data: step.memory.as_bytes().to_vec().into(),
})
};
// Calculate the stack items at this step
let push_stack = {
let step_op = step.op.get();
let show_stack: usize;
if (opcode::PUSH0..=opcode::PUSH32).contains(&step_op) {
show_stack = 1;
} else if (opcode::SWAP1..=opcode::SWAP16).contains(&step_op) {
show_stack = (step_op - opcode::SWAP1) as usize + 2;
} else if (opcode::DUP1..=opcode::DUP16).contains(&step_op) {
show_stack = (step_op - opcode::DUP1) as usize + 2;
} else {
show_stack = match step_op {
opcode::CALLDATALOAD |
opcode::SLOAD |
opcode::MLOAD |
opcode::CALLDATASIZE |
opcode::LT |
opcode::GT |
opcode::DIV |
opcode::SDIV |
opcode::SAR |
opcode::AND |
opcode::EQ |
opcode::CALLVALUE |
opcode::ISZERO |
opcode::ADD |
opcode::EXP |
opcode::CALLER |
opcode::KECCAK256 |
opcode::SUB |
opcode::ADDRESS |
opcode::GAS |
opcode::MUL |
opcode::RETURNDATASIZE |
opcode::NOT |
opcode::SHR |
opcode::SHL |
opcode::EXTCODESIZE |
opcode::SLT |
opcode::OR |
opcode::NUMBER |
opcode::PC |
opcode::TIMESTAMP |
opcode::BALANCE |
opcode::SELFBALANCE |
opcode::MULMOD |
opcode::ADDMOD |
opcode::BASEFEE |
opcode::BLOCKHASH |
opcode::BYTE |
opcode::XOR |
opcode::ORIGIN |
opcode::CODESIZE |
opcode::MOD |
opcode::SIGNEXTEND |
opcode::GASLIMIT |
opcode::DIFFICULTY |
opcode::SGT |
opcode::GASPRICE |
opcode::MSIZE |
opcode::EXTCODEHASH |
opcode::SMOD |
opcode::CHAINID |
opcode::COINBASE => 1,
_ => 0,
}
};
let mut push_stack = step.push_stack.clone().unwrap_or_default();
for idx in (0..show_stack).rev() {
if step.stack.len() > idx {
push_stack.push(step.stack.peek(idx).unwrap_or_default())
}
}
push_stack
};
let maybe_execution = Some(VmExecutedOperation {
used: step.gas_remaining,
push: push_stack,
mem: maybe_memory,
store: maybe_storage,
});
let cost = self
.spec_id
.and_then(|spec_id| {
spec_opcode_gas(spec_id).get(step.op.get() as usize).map(|op| op.get_gas())
})
.unwrap_or_default();
VmInstruction {
pc: step.pc,
cost: cost as u64,
ex: maybe_execution,
sub: maybe_sub_call,
op: Some(step.op.to_string()),
idx: None,
}
}
}
/// An iterator for [TransactionTrace]s
///
/// This iterator handles additional selfdestruct actions based on the last emitted
/// [TransactionTrace], since selfdestructs are not recorded as individual call traces but are
/// derived from recorded call
struct TransactionTraceIter<Iter> {
iter: Iter,
next_selfdestruct: Option<TransactionTrace>,
}
impl<Iter> Iterator for TransactionTraceIter<Iter>
where
Iter: Iterator<Item = (TransactionTrace, CallTraceNode)>,
{
type Item = TransactionTrace;
fn next(&mut self) -> Option<Self::Item> {
if let Some(selfdestruct) = self.next_selfdestruct.take() {
return Some(selfdestruct)
}
let (mut trace, node) = self.iter.next()?;
if node.is_selfdestruct() {
// since selfdestructs are emitted as additional trace, increase the trace count
let mut addr = trace.trace_address.clone();
addr.push(trace.subtraces);
// need to account for the additional selfdestruct trace
trace.subtraces += 1;
self.next_selfdestruct = node.parity_selfdestruct_trace(addr);
}
Some(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, I>(
db: DB,
trace: &mut VmTrace,
breadth_first_addresses: I,
) -> Result<(), DB::Error>
where
DB: DatabaseRef,
I: IntoIterator<Item = Address>,
{
let mut stack: VecDeque<&mut VmTrace> = VecDeque::new();
stack.push_back(trace);
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_ref(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_ref(code_hash)?.original_bytes();
}
Ok(())
}
/// Loops over all state accounts in the accounts diff that contains all accounts that are included
/// in the [ExecutionResult] state map and compares the balance and nonce against what's in the
/// `db`, which should point to the beginning of the transaction.
///
/// It's expected that `DB` is a revm [Database](revm::db::Database) which at this point already
/// contains all the accounts that are in the state map and never has to fetch them from disk.
pub fn populate_state_diff<'a, DB, I>(
state_diff: &mut StateDiff,
db: DB,
account_diffs: I,
) -> Result<(), DB::Error>
where
I: IntoIterator<Item = (&'a Address, &'a Account)>,
DB: DatabaseRef,
{
for (addr, changed_acc) in account_diffs.into_iter() {
// if the account was selfdestructed and created during the transaction, we can ignore it
if changed_acc.is_selfdestructed() && changed_acc.is_created() {
continue
}
let addr = *addr;
let entry = state_diff.entry(addr).or_default();
// we check if this account was created during the transaction
if changed_acc.is_created() || changed_acc.is_loaded_as_not_existing() {
entry.balance = Delta::Added(changed_acc.info.balance);
entry.nonce = Delta::Added(U64::from(changed_acc.info.nonce));
if changed_acc.info.code_hash == KECCAK_EMPTY {
// this is an additional check to ensure new accounts always get the empty code
// marked as added
entry.code = Delta::Added(Default::default());
}
// new storage values
for (key, slot) in changed_acc.storage.iter() {
entry.storage.insert((*key).into(), Delta::Added(slot.present_value.into()));
}
} else {
// account already exists, we need to fetch the account from the db
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
// update _changed_ storage values
for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed()) {
entry.storage.insert(
(*key).into(),
Delta::changed(
slot.previous_or_original_value.into(),
slot.present_value.into(),
),
);
}
// check if the account was changed at all
if entry.storage.is_empty() &&
db_acc == changed_acc.info &&
!changed_acc.is_selfdestructed()
{
// clear the entry if the account was not changed
state_diff.remove(&addr);
continue
}
entry.balance = if db_acc.balance == changed_acc.info.balance {
Delta::Unchanged
} else {
Delta::Changed(ChangedType { from: db_acc.balance, to: changed_acc.info.balance })
};
// this is relevant for the caller and contracts
entry.nonce = if db_acc.nonce == changed_acc.info.nonce {
Delta::Unchanged
} else {
Delta::Changed(ChangedType {
from: U64::from(db_acc.nonce),
to: U64::from(changed_acc.info.nonce),
})
};
}
}
Ok(())
}