use std::fmt::Debug;
use alloy_primitives::{Address, Log, B256, U256};
use arena::{CallTraceArena, PushTraceKind};
use brontes_types::structured_trace::{TransactionTraceWithLogs, TxTrace};
use config::TracingInspectorConfig;
use reth_primitives::{Bytes, U64};
use reth_rpc_types::{trace::parity::*, TransactionInfo};
use revm::{
inspectors::GasInspector,
interpreter::{
opcode, CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome,
InstructionResult, Interpreter, InterpreterResult, OpCode,
},
primitives::{ExecutionResult, SpecId},
Database, EvmContext, Inspector, JournalEntry,
};
use types::{
CallKind, CallTrace, CallTraceNode, CallTraceStep, LogCallOrder, RecordedMemory, StorageChange,
StorageChangeReason,
};
use utils::{gas_used, stack_push_count};
use super::{arena, config, types, utils};
#[derive(Clone, Debug)]
pub struct BrontesTracingInspector {
pub config: TracingInspectorConfig,
pub traces: CallTraceArena,
pub trace_stack: Vec<usize>,
pub step_stack: Vec<StackStep>,
pub last_call_return_data: Option<Bytes>,
pub gas_inspector: GasInspector,
pub spec_id: Option<SpecId>,
}
impl BrontesTracingInspector {
pub fn new(config: TracingInspectorConfig) -> Self {
Self {
config,
traces: Default::default(),
trace_stack: vec![],
step_stack: vec![],
last_call_return_data: None,
gas_inspector: Default::default(),
spec_id: None,
}
}
pub const fn config(&self) -> &TracingInspectorConfig {
&self.config
}
pub const fn get_traces(&self) -> &CallTraceArena {
&self.traces
}
pub fn get_traces_mut(&mut self) -> &mut CallTraceArena {
&mut self.traces
}
#[inline]
pub fn set_transaction_gas_used(&mut self, gas_used: u64) {
if let Some(node) = self.traces.arena.first_mut() {
node.trace.gas_used = gas_used;
}
}
#[inline]
pub fn with_transaction_gas_used(mut self, gas_used: u64) -> Self {
self.set_transaction_gas_used(gas_used);
self
}
fn is_deep(&self) -> bool {
!self.trace_stack.is_empty()
}
#[inline]
fn is_precompile_call<DB: Database>(
&self,
context: &EvmContext<DB>,
to: &Address,
value: U256,
) -> bool {
if context.precompiles.contains_key(to) {
return self.is_deep() && value.is_zero()
}
false
}
#[track_caller]
#[inline]
fn active_trace(&self) -> Option<&CallTraceNode> {
self.trace_stack.last().map(|idx| &self.traces.arena[*idx])
}
#[track_caller]
#[inline]
fn last_trace_idx(&self) -> usize {
self.trace_stack
.last()
.copied()
.expect("can't start step without starting a trace first")
}
#[track_caller]
#[inline]
fn pop_trace_idx(&mut self) -> usize {
self.trace_stack
.pop()
.expect("more traces were filled than started")
}
#[allow(clippy::too_many_arguments)]
fn start_trace_on_call<DB: Database>(
&mut self,
context: &EvmContext<DB>,
address: Address,
input_data: Bytes,
value: U256,
kind: CallKind,
caller: Address,
mut gas_limit: u64,
maybe_precompile: Option<bool>,
) {
let push_kind = if maybe_precompile.unwrap_or(false) {
PushTraceKind::PushOnly
} else {
PushTraceKind::PushAndAttachToParent
};
if self.trace_stack.is_empty() {
gas_limit = context.env.tx.gas_limit;
self.spec_id = Some(context.spec_id());
}
self.trace_stack.push(self.traces.push_trace(
0,
push_kind,
CallTrace {
depth: context.journaled_state.depth() as usize,
address,
kind,
data: input_data,
value,
status: InstructionResult::Continue,
caller,
maybe_precompile,
gas_limit,
..Default::default()
},
));
}
fn fill_trace_on_call_end<DB: Database>(
&mut self,
context: &mut EvmContext<DB>,
result: InterpreterResult,
created_address: Option<Address>,
) {
let InterpreterResult { result, output, gas } = result;
let trace_idx = self.pop_trace_idx();
let trace = &mut self.traces.arena[trace_idx].trace;
if trace_idx == 0 {
trace.gas_used = gas_used(context.spec_id(), gas.spent(), gas.refunded() as u64);
} else {
trace.gas_used = gas.spent();
}
trace.status = result;
trace.success = trace.status.is_ok();
trace.output = output.clone();
self.last_call_return_data = Some(output);
if let Some(address) = created_address {
trace.address = address;
}
}
fn start_step<DB: Database>(&mut self, interp: &mut Interpreter, context: &mut EvmContext<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 memory = self
.config
.record_memory_snapshots
.then(|| RecordedMemory::new(interp.shared_memory.context_memory().to_vec()))
.unwrap_or_default();
let stack = if self.config.record_stack_snapshots.is_full() {
Some(interp.stack.data().clone())
} else {
None
};
let op = unsafe { OpCode::new_unchecked(interp.current_opcode()) };
trace.trace.steps.push(CallTraceStep {
depth: context.journaled_state.depth(),
pc: interp.program_counter(),
op,
contract: interp.contract.address,
stack,
push_stack: None,
memory_size: memory.len(),
memory,
gas_remaining: self.gas_inspector.gas_remaining(),
gas_refund_counter: interp.gas.refunded() as u64,
gas_cost: 0,
storage_change: None,
status: InstructionResult::Continue,
});
}
fn fill_step_on_step_end<DB: Database>(
&mut self,
interp: &Interpreter,
context: &EvmContext<DB>,
) {
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 self.config.record_stack_snapshots.is_pushes() {
let num_pushed = stack_push_count(step.op);
let start = interp.stack.len() - num_pushed;
step.push_stack = Some(interp.stack.data()[start..].to_vec());
}
if self.config.record_memory_snapshots {
if interp.shared_memory.len() > step.memory.len() {
step.memory.resize(interp.shared_memory.len());
}
}
if self.config.record_state_diff {
let op = step.op.get();
let journal_entry = context
.journaled_state
.journal
.last()
.expect("exists; initialized with vec")
.last();
step.storage_change = match (op, journal_entry) {
(
opcode::SLOAD | opcode::SSTORE,
Some(JournalEntry::StorageChange { address, key, had_value }),
) => {
let value = context.journaled_state.state[address].storage[key].present_value();
let reason = match op {
opcode::SLOAD => StorageChangeReason::SLOAD,
opcode::SSTORE => StorageChangeReason::SSTORE,
_ => unreachable!(),
};
let change = StorageChange { key: *key, value, had_value: *had_value, reason };
Some(change)
}
_ => None,
};
}
step.gas_cost = step
.gas_remaining
.saturating_sub(self.gas_inspector.gas_remaining());
step.status = interp.instruction_result;
}
}
impl BrontesTracingInspector {
pub fn into_trace_results(self, info: TransactionInfo, res: &ExecutionResult) -> TxTrace {
let gas_used = res.gas_used().into();
let trace = self.build_trace(info.hash.unwrap(), info.block_number.unwrap());
TxTrace {
block_number: info.block_number.unwrap_or_default(),
trace: trace.unwrap_or_default(),
tx_hash: info.hash.unwrap(),
gas_used,
effective_price: 0,
tx_index: info.index.unwrap(),
is_success: res.is_success(),
}
}
fn iter_traceable_nodes(&self) -> impl Iterator<Item = &CallTraceNode> {
self.traces
.nodes()
.iter()
.filter(|node| !node.trace.maybe_precompile.unwrap_or(false))
}
pub fn build_trace(
&self,
tx_hash: B256,
block_number: u64,
) -> Option<Vec<TransactionTraceWithLogs>> {
if self.traces.nodes().is_empty() {
return None
}
let mut traces: Vec<TransactionTraceWithLogs> =
Vec::with_capacity(self.traces.nodes().len());
for node in self.iter_traceable_nodes() {
let trace_address = self.trace_address(self.traces.nodes(), node.idx);
let trace = self.build_tx_trace(node, trace_address);
let logs = node
.logs
.iter()
.map(|log| Log { address: node.trace.address, data: log.clone() })
.collect::<Vec<_>>();
let msg_sender = if let Action::Call(c) = &trace.action {
if c.call_type == CallType::DelegateCall {
if let Some(prev_trace) = traces.iter().rev().find(|n| match &n.trace.action {
Action::Call(c) => c.call_type != CallType::DelegateCall,
Action::Create(_) => true,
_ => false,
}) {
prev_trace.msg_sender
} else {
tracing::error!(
target: "brontes::tracing",
?block_number,
?tx_hash,
"couldn't find head of delegate call for block"
);
panic!("should never be reached");
}
} else {
match &trace.action {
Action::Call(call) => call.from,
Action::Create(call) => call.from,
Action::Reward(call) => call.author,
Action::Selfdestruct(call) => call.address,
}
}
} else {
match &trace.action {
Action::Call(call) => call.from,
Action::Create(call) => call.from,
Action::Reward(call) => call.author,
Action::Selfdestruct(call) => call.address,
}
};
traces.push(TransactionTraceWithLogs {
trace,
logs,
msg_sender,
decoded_data: None,
trace_idx: node.idx as u64,
});
if node.trace.status == InstructionResult::SelfDestruct {
let addr = {
let last = traces.last_mut().expect("exists");
let mut addr = last.trace.trace_address.clone();
addr.push(last.trace.subtraces);
last.trace.subtraces += 1;
addr
};
if let Some(trace) = self.parity_selfdestruct_trace(node, addr) {
traces.push(TransactionTraceWithLogs {
msg_sender,
trace,
logs: vec![],
decoded_data: None,
trace_idx: node.idx as u64,
});
}
}
}
Some(traces)
}
fn trace_address(&self, nodes: &[CallTraceNode], idx: usize) -> Vec<usize> {
if idx == 0 {
return vec![]
}
let mut graph = vec![];
let mut node = &nodes[idx];
if node.trace.maybe_precompile.unwrap_or(false) {
return graph
}
while let Some(parent) = node.parent {
let child_idx = node.idx;
node = &nodes[parent];
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
}
pub(crate) fn parity_selfdestruct_trace(
&self,
node: &CallTraceNode,
trace_address: Vec<usize>,
) -> Option<TransactionTrace> {
let trace = self.parity_selfdestruct_action(node)?;
Some(TransactionTrace {
action: trace,
error: None,
result: None,
trace_address,
subtraces: 0,
})
}
pub(crate) fn parity_selfdestruct_action(&self, node: &CallTraceNode) -> Option<Action> {
if node.trace.selfdestruct_refund_target.is_some() {
Some(Action::Selfdestruct(SelfdestructAction {
address: node.trace.address,
refund_address: node.trace.selfdestruct_refund_target.unwrap_or_default(),
balance: node.trace.value,
}))
} else {
None
}
}
pub(crate) fn parity_action(&self, node: &CallTraceNode) -> Action {
match node.trace.kind {
CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => {
Action::Call(CallAction {
from: node.trace.caller,
to: node.trace.address,
value: node.trace.value,
gas: U64::from(node.trace.gas_limit),
input: node.trace.data.clone(),
call_type: node.trace.kind.into(),
})
}
CallKind::Create | CallKind::Create2 => Action::Create(CreateAction {
from: node.trace.caller,
value: node.trace.value,
gas: U64::from(node.trace.gas_limit),
init: node.trace.data.clone(),
}),
}
}
pub(crate) fn parity_trace_output(&self, node: &CallTraceNode) -> TraceOutput {
match node.trace.kind {
CallKind::Call | CallKind::StaticCall | CallKind::CallCode | CallKind::DelegateCall => {
TraceOutput::Call(CallOutput {
gas_used: U64::from(node.trace.gas_used),
output: node.trace.output.clone(),
})
}
CallKind::Create | CallKind::Create2 => TraceOutput::Create(CreateOutput {
gas_used: U64::from(node.trace.gas_used),
code: node.trace.output.clone(),
address: node.trace.address,
}),
}
}
pub(crate) fn as_error_msg(&self, node: &CallTraceNode) -> Option<String> {
node.trace.is_error().then(|| match node.trace.status {
InstructionResult::Revert => "execution reverted".to_string(),
InstructionResult::OutOfGas | InstructionResult::MemoryOOG => "out of gas".to_string(),
InstructionResult::OpcodeNotFound => "invalid opcode".to_string(),
InstructionResult::StackOverflow => "Out of stack".to_string(),
InstructionResult::InvalidJump => "invalid jump destination".to_string(),
InstructionResult::PrecompileError => "precompiled failed".to_string(),
status => format!("{:?}", status),
})
}
pub fn build_tx_trace(
&self,
node: &CallTraceNode,
trace_address: Vec<usize>,
) -> TransactionTrace {
let action = self.parity_action(node);
let result = if node.trace.is_error() && !node.trace.is_revert() {
None
} else {
Some(self.parity_trace_output(node))
};
let error = self.as_error_msg(node);
TransactionTrace { action, error, result, trace_address, subtraces: node.children.len() }
}
}
impl<DB> Inspector<DB> for BrontesTracingInspector
where
DB: Database,
{
#[inline]
fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut EvmContext<DB>) {
self.gas_inspector.initialize_interp(interp, context)
}
fn step(&mut self, interp: &mut Interpreter, context: &mut EvmContext<DB>) {
self.gas_inspector.step(interp, context);
if self.config.record_steps {
self.start_step(interp, context);
}
}
fn step_end(&mut self, interp: &mut Interpreter, context: &mut EvmContext<DB>) {
self.gas_inspector.step_end(interp, context);
if self.config.record_steps {
self.fill_step_on_step_end(interp, context);
}
}
fn log(&mut self, context: &mut EvmContext<DB>, log: &Log) {
self.gas_inspector.log(context, log);
let trace_idx = self.last_trace_idx();
let trace = &mut self.traces.arena[trace_idx];
if self.config.record_logs {
trace.ordering.push(LogCallOrder::Log(trace.logs.len()));
trace.logs.push(log.data.clone());
}
}
fn call(
&mut self,
context: &mut EvmContext<DB>,
inputs: &mut CallInputs,
) -> Option<CallOutcome> {
self.gas_inspector.call(context, inputs);
let (from, to) = match inputs.context.scheme {
CallScheme::DelegateCall | CallScheme::CallCode => {
(inputs.context.address, inputs.context.code_address)
}
_ => (inputs.context.caller, inputs.context.address),
};
let value = if matches!(inputs.context.scheme, CallScheme::DelegateCall) {
if let Some(parent) = self.active_trace() {
parent.trace.value
} else {
inputs.transfer.value
}
} else {
inputs.transfer.value
};
let maybe_precompile = self
.config
.exclude_precompile_calls
.then(|| self.is_precompile_call(context, &to, value));
self.start_trace_on_call(
context,
to,
inputs.input.clone(),
value,
inputs.context.scheme.into(),
from,
inputs.gas_limit,
maybe_precompile,
);
None
}
fn call_end(
&mut self,
context: &mut EvmContext<DB>,
inputs: &CallInputs,
outcome: CallOutcome,
) -> CallOutcome {
let outcome = self.gas_inspector.call_end(context, inputs, outcome);
self.fill_trace_on_call_end(context, outcome.result.clone(), None);
outcome
}
fn create(
&mut self,
context: &mut EvmContext<DB>,
inputs: &mut CreateInputs,
) -> Option<CreateOutcome> {
self.gas_inspector.create(context, inputs);
let _ = context.load_account(inputs.caller);
let nonce = context.journaled_state.account(inputs.caller).info.nonce;
self.start_trace_on_call(
context,
inputs.created_address(nonce),
inputs.init_code.clone(),
inputs.value,
inputs.scheme.into(),
inputs.caller,
inputs.gas_limit,
Some(false),
);
None
}
fn create_end(
&mut self,
context: &mut EvmContext<DB>,
inputs: &CreateInputs,
outcome: CreateOutcome,
) -> CreateOutcome {
let outcome = self.gas_inspector.create_end(context, inputs, outcome);
self.fill_trace_on_call_end(context, outcome.result.clone(), outcome.address);
outcome
}
fn selfdestruct(&mut self, _contract: Address, target: Address, _value: U256) {
let trace_idx = self.last_trace_idx();
let trace = &mut self.traces.arena[trace_idx].trace;
trace.selfdestruct_refund_target = Some(target)
}
}
#[derive(Clone, Copy, Debug)]
pub struct StackStep {
trace_idx: usize,
step_idx: usize,
}
impl From<CallKind> for CallType {
fn from(item: CallKind) -> Self {
match item {
CallKind::Call => CallType::Call,
CallKind::StaticCall => CallType::StaticCall,
CallKind::CallCode => CallType::CallCode,
CallKind::DelegateCall => CallType::DelegateCall,
CallKind::Create | CallKind::Create2 => CallType::None,
}
}
}