use crate::tracing::{
types::{CallKind, CallTraceNode, CallTraceStepStackItem},
utils::load_account_code,
};
use alloc::{
borrow::Cow,
collections::{BTreeMap, VecDeque},
format, vec,
vec::Vec,
};
use alloy_primitives::{
map::{Entry, HashMap},
Address, Bytes, B256, U256,
};
use alloy_rpc_types_trace::geth::{
erc7562::{AccessedSlots, CallFrameType, ContractSize, Erc7562Config, Erc7562Frame},
AccountChangeKind, AccountState, CallConfig, CallFrame, DefaultFrame, DiffMode,
GethDefaultTracingOptions, PreStateConfig, PreStateFrame, PreStateMode, StructLog,
};
use revm::{
bytecode::opcode,
context_interface::result::{HaltReasonTr, ResultAndState},
primitives::KECCAK_EMPTY,
state::{AccountInfo, EvmState},
DatabaseRef,
};
#[derive(Clone, Debug)]
pub struct GethTraceBuilder<'a> {
nodes: Cow<'a, [CallTraceNode]>,
}
impl GethTraceBuilder<'static> {
pub fn new(nodes: Vec<CallTraceNode>) -> GethTraceBuilder<'static> {
Self { nodes: Cow::Owned(nodes) }
}
}
impl<'a> GethTraceBuilder<'a> {
pub fn new_borrowed(nodes: &'a [CallTraceNode]) -> GethTraceBuilder<'a> {
Self { nodes: Cow::Borrowed(nodes) }
}
pub fn to_owned(self) -> Vec<CallTraceNode> {
self.nodes.into_owned()
}
fn trace_step_count(&self) -> usize {
self.nodes.iter().map(|node| node.trace.steps.len()).sum()
}
fn fill_geth_trace(
&self,
main_trace_node: &CallTraceNode,
opts: &GethDefaultTracingOptions,
storage: &mut HashMap<Address, BTreeMap<B256, B256>>,
struct_logs: &mut Vec<StructLog>,
) {
let mut step_stack = VecDeque::with_capacity(main_trace_node.trace.steps.len());
main_trace_node.push_steps_on_stack(&mut step_stack);
while let Some(CallTraceStepStackItem { trace_node, step, call_child_id }) =
step_stack.pop_back()
{
let mut log = step.convert_to_geth_struct_log(opts, trace_node.trace.depth as u64 + 1);
if opts.is_storage_enabled() {
let contract_storage = storage.entry(trace_node.execution_address()).or_default();
if let Some(change) = &step.storage_change {
contract_storage.insert(change.key.into(), change.value.into());
}
if matches!(step.op.get(), opcode::SLOAD | opcode::SSTORE) {
log.storage = Some(contract_storage.clone());
}
}
if opts.is_return_data_enabled() {
log.return_data = Some(step.returndata.clone());
}
struct_logs.push(log);
if let Some(call_child_id) = call_child_id {
let child_trace = &self.nodes[call_child_id];
child_trace.push_steps_on_stack(&mut step_stack);
}
}
}
pub fn geth_traces(
&self,
receipt_gas_used: u64,
return_value: Bytes,
opts: GethDefaultTracingOptions,
) -> DefaultFrame {
if self.nodes.is_empty() {
return Default::default();
}
let main_trace_node = &self.nodes[0];
let main_trace = &main_trace_node.trace;
let mut struct_logs = Vec::with_capacity(self.trace_step_count());
let mut storage = HashMap::default();
self.fill_geth_trace(main_trace_node, &opts, &mut storage, &mut struct_logs);
DefaultFrame {
failed: !main_trace.success,
gas: receipt_gas_used,
return_value,
struct_logs,
}
}
pub fn geth_call_traces(&self, opts: CallConfig, gas_used: u64) -> CallFrame {
if self.nodes.is_empty() {
return Default::default();
}
let include_logs = opts.with_log.unwrap_or_default();
let main_trace_node = &self.nodes[0];
let mut root_call_frame = main_trace_node.geth_empty_call_frame(include_logs);
root_call_frame.gas_used = U256::from(gas_used);
if let Some(selfdestruct) = main_trace_node.geth_selfdestruct_call_trace() {
root_call_frame.calls.push(selfdestruct);
}
if opts.only_top_call.unwrap_or_default() {
return root_call_frame;
}
let mut call_frames = Vec::with_capacity(self.nodes.len());
call_frames.push((0, root_call_frame));
for (idx, trace) in self.nodes.iter().enumerate().skip(1) {
let include_logs = include_logs && !self.call_or_parent_failed(trace);
call_frames.push((idx, trace.geth_empty_call_frame(include_logs)));
if let Some(selfdestruct) = trace.geth_selfdestruct_call_trace() {
call_frames.last_mut().expect("not empty").1.calls.push(selfdestruct);
}
}
loop {
let (idx, call) = call_frames.pop().expect("call frames not empty");
let node = &self.nodes[idx];
if let Some(parent) = node.parent {
let parent_frame = &mut call_frames[parent];
parent_frame.1.calls.insert(0, call);
} else {
debug_assert!(call_frames.is_empty(), "only one root node has no parent");
return call;
}
}
}
fn call_or_parent_failed(&self, node: &CallTraceNode) -> bool {
if node.trace.is_error() {
return true;
}
let mut parent_idx = node.parent;
while let Some(idx) = parent_idx {
let next = &self.nodes[idx];
if next.trace.is_error() {
return true;
}
parent_idx = next.parent;
}
false
}
pub fn geth_prestate_traces<DB: DatabaseRef>(
&self,
ResultAndState { state, .. }: &ResultAndState<impl HaltReasonTr>,
prestate_config: &PreStateConfig,
db: DB,
) -> Result<PreStateFrame, DB::Error> {
let code_enabled = prestate_config.code_enabled();
let storage_enabled = prestate_config.storage_enabled();
if prestate_config.is_diff_mode() {
self.geth_prestate_diff_traces(state, db, code_enabled, storage_enabled)
} else {
self.geth_prestate_pre_traces(state, db, code_enabled, storage_enabled)
}
}
fn geth_prestate_pre_traces<DB: DatabaseRef>(
&self,
state: &EvmState,
db: DB,
code_enabled: bool,
storage_enabled: bool,
) -> Result<PreStateFrame, DB::Error> {
let account_diffs = state.iter().map(|(addr, acc)| (*addr, acc));
let mut prestate = PreStateMode::default();
for (addr, changed_acc) in account_diffs {
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
let code = code_enabled.then(|| load_account_code(&db, &db_acc)).flatten();
let mut acc_state = AccountState::from_account_info(db_acc.nonce, db_acc.balance, code);
if storage_enabled {
for (key, slot) in changed_acc.storage.iter() {
acc_state.storage.insert((*key).into(), slot.original_value.into());
}
}
prestate.0.insert(addr, acc_state);
}
Ok(PreStateFrame::Default(prestate))
}
fn geth_prestate_diff_traces<DB: DatabaseRef>(
&self,
state: &EvmState,
db: DB,
code_enabled: bool,
storage_enabled: bool,
) -> Result<PreStateFrame, DB::Error> {
let account_diffs = state.iter().map(|(addr, acc)| (*addr, acc));
let mut state_diff = DiffMode::default();
let mut account_change_kinds =
HashMap::with_capacity_and_hasher(account_diffs.len(), Default::default());
for (addr, changed_acc) in account_diffs {
let db_acc = db.basic_ref(addr)?.unwrap_or_default();
let pre_code = code_enabled.then(|| load_account_code(&db, &db_acc)).flatten();
let mut post_state = AccountState::from_account_info(
changed_acc.info.nonce,
changed_acc.info.balance,
code_enabled
.then(|| {
if changed_acc.info.code_hash == db_acc.code_hash {
pre_code.clone()
} else {
changed_acc.info.code.as_ref().map(|code| code.original_bytes())
}
})
.flatten(),
);
let mut pre_state =
AccountState::from_account_info(db_acc.nonce, db_acc.balance, pre_code);
if storage_enabled {
for (key, slot) in changed_acc.storage.iter().filter(|(_, slot)| slot.is_changed())
{
pre_state.storage.insert((*key).into(), slot.original_value.into());
post_state.storage.insert((*key).into(), slot.present_value.into());
}
}
state_diff.pre.insert(addr, pre_state);
let pre_change = if changed_acc.is_created() && account_was_empty(&db_acc) {
AccountChangeKind::Create
} else {
AccountChangeKind::Modify
};
let post_change = if changed_acc.is_selfdestructed() {
AccountChangeKind::SelfDestruct
} else {
AccountChangeKind::Modify
};
account_change_kinds.insert(addr, (pre_change, post_change));
if !changed_acc.is_selfdestructed() {
state_diff.post.insert(addr, post_state);
}
}
state_diff.retain_changed().remove_zero_storage_values();
self.diff_traces(&mut state_diff.pre, &mut state_diff.post, account_change_kinds);
Ok(PreStateFrame::Diff(state_diff))
}
fn diff_traces(
&self,
pre: &mut BTreeMap<Address, AccountState>,
post: &mut BTreeMap<Address, AccountState>,
change_type: HashMap<Address, (AccountChangeKind, AccountChangeKind)>,
) {
post.retain(|addr, post_state| {
if change_type.get(addr).map(|ty| ty.1.is_selfdestruct()).unwrap_or(false) {
return false;
}
if let Some(pre_state) = pre.get(addr) {
post_state.remove_matching_account_info(pre_state);
}
true
});
pre.retain(|addr, _pre_state| {
change_type.get(addr).map(|ty| !ty.0.is_created()).unwrap_or(true)
});
}
pub fn geth_erc7562_traces<DB: DatabaseRef>(
&self,
opts: Erc7562Config,
gas_used: u64,
db: DB,
) -> Erc7562Frame {
if self.nodes.is_empty() {
return Default::default();
}
let include_logs = opts.with_log.unwrap_or_default();
let call_config = CallConfig { only_top_call: None, with_log: Some(include_logs) };
let mut top_call = Some(self.geth_call_traces(call_config, gas_used));
let mut frames: Vec<(usize, Erc7562Frame)> = Vec::with_capacity(self.nodes.len());
for (idx, node) in self.nodes.iter().enumerate() {
let trace = &node.trace;
let mut accessed_slots = AccessedSlots::default();
let mut used_opcodes = HashMap::default();
let mut contract_size = HashMap::default();
let mut ext_code_access_info = Vec::new();
let mut keccak = Vec::new();
let mut out_of_gas = false;
for step in &trace.steps {
let op = step.op.get();
if opts.ignored_opcodes.contains(&op) {
continue;
}
*used_opcodes.entry(op).or_insert(0) += 1;
match op {
opcode::SLOAD => {
if let Some(stack) = &step.stack {
if let Some(slot) = stack.get(stack.len().saturating_sub(1)) {
let slot: B256 = (*slot).into();
let already_read = accessed_slots.reads.contains_key(&slot);
let already_written = accessed_slots.writes.contains_key(&slot);
if !already_read && !already_written {
if let Some(change) = &step.storage_change {
let value: B256 = change.value.into();
accessed_slots.reads.entry(slot).or_default().push(value);
}
}
}
}
}
opcode::SSTORE => {
if let Some(stack) = &step.stack {
if let Some(slot) = stack.get(stack.len().saturating_sub(1)) {
let slot: B256 = (*slot).into();
*accessed_slots.writes.entry(slot).or_insert(0) += 1;
}
}
}
opcode::TLOAD => {
if let Some(stack) = &step.stack {
if let Some(slot) = stack.get(stack.len().saturating_sub(1)) {
let slot: B256 = (*slot).into();
*accessed_slots.transient_reads.entry(slot).or_insert(0) += 1;
}
}
}
opcode::TSTORE => {
if let Some(stack) = &step.stack {
if let Some(slot) = stack.get(stack.len().saturating_sub(1)) {
let slot: B256 = (*slot).into();
*accessed_slots.transient_writes.entry(slot).or_insert(0) += 1;
}
}
}
_ => {}
}
if let Some(status) = &step.status {
if *status == revm::interpreter::InstructionResult::OutOfGas {
out_of_gas = true;
}
}
if matches!(op, opcode::EXTCODESIZE | opcode::EXTCODECOPY | opcode::EXTCODEHASH) {
if let Some(stack) = &step.stack {
if let Some(item) = stack.get(stack.len().saturating_sub(1)) {
let address = Address::from_word((*item).into());
ext_code_access_info.push(format!("{address:?}"));
if let Entry::Vacant(e) = contract_size.entry(address) {
if let Ok(Some(account)) = db.basic_ref(address) {
if account.code_hash != KECCAK_EMPTY {
if let Ok(bytecode) = db.code_by_hash_ref(account.code_hash)
{
e.insert(ContractSize {
contract_size: bytecode.original_bytes().len()
as u64,
opcode: op,
});
}
}
}
}
}
}
}
if op == opcode::KECCAK256 && !out_of_gas {
if let (Some(stack), Some(memory)) = (&step.stack, &step.memory) {
if stack.len() >= 2 {
let offset = stack[stack.len() - 1];
let len = stack[stack.len() - 2];
if let (Ok(offset), Ok(len)) =
(usize::try_from(offset), usize::try_from(len))
{
let mut data = vec![0; len];
if offset < memory.0.len() {
let end = (offset + len).min(memory.0.len());
let copy_len = end - offset;
data[..copy_len].copy_from_slice(&memory.0[offset..end]);
}
keccak.push(Bytes::from(data));
}
}
}
}
}
let call_frame = if idx == 0 {
top_call.take().unwrap()
} else {
let include_logs = include_logs && !self.call_or_parent_failed(node);
self.nodes[idx].geth_empty_call_frame(include_logs)
};
let call_frame_type = Self::convert_call_kind(node.kind());
frames.push((
idx,
Erc7562Frame {
call_frame_type,
from: call_frame.from,
gas: call_frame.gas.to(),
gas_used: call_frame.gas_used.to(),
to: call_frame.to,
input: call_frame.input,
output: call_frame.output,
error: call_frame.error,
revert_reason: call_frame.revert_reason,
logs: call_frame.logs,
value: call_frame.value,
accessed_slots,
ext_code_access_info,
used_opcodes,
contract_size,
out_of_gas,
keccak,
calls: vec![],
},
));
}
loop {
let (idx, frame) = frames.pop().expect("call frames not empty");
let node = &self.nodes[idx];
if let Some(parent) = node.parent {
let parent_frame = &mut frames[parent];
parent_frame.1.calls.insert(0, frame);
} else {
debug_assert!(frames.is_empty(), "only one root node has no parent");
return frame;
}
}
}
pub fn convert_call_kind(kind: CallKind) -> CallFrameType {
match kind {
CallKind::Call => CallFrameType::Call,
CallKind::CallCode => CallFrameType::CallCode,
CallKind::DelegateCall => CallFrameType::DelegateCall,
CallKind::StaticCall => CallFrameType::StaticCall,
CallKind::Create => CallFrameType::Create,
CallKind::Create2 => CallFrameType::Create2,
CallKind::AuthCall => CallFrameType::Call,
}
}
}
fn account_was_empty(account: &AccountInfo) -> bool {
account.balance.is_zero() && account.nonce == 0 && account.code_hash == KECCAK_EMPTY
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{address, U256};
use revm::{
database::CacheDB,
database_interface::EmptyDB,
state::{Account, AccountInfo},
};
#[test]
fn prestate_diff_keeps_prefunded_created_accounts() {
let mut state = EvmState::default();
let prefunded_addr = address!("1000000000000000000000000000000000000001");
let empty_addr = address!("2000000000000000000000000000000000000002");
let mut prefunded_account = Account::default();
prefunded_account.mark_created();
prefunded_account.info.balance = U256::from(1);
prefunded_account.info.nonce = 1;
state.insert(prefunded_addr, prefunded_account);
let mut empty_account = Account::default();
empty_account.mark_created();
empty_account.info.nonce = 1;
state.insert(empty_addr, empty_account);
let mut db = CacheDB::new(EmptyDB::default());
db.insert_account_info(
prefunded_addr,
AccountInfo { balance: U256::from(10), ..Default::default() },
);
let builder = GethTraceBuilder::new(Vec::new());
let frame =
builder.geth_prestate_diff_traces(&state, db, false, false).expect("diff frame");
match frame {
PreStateFrame::Diff(diff) => {
assert!(
diff.pre.contains_key(&prefunded_addr),
"prefunded contract must remain in prestate diff"
);
assert!(
!diff.pre.contains_key(&empty_addr),
"contracts created on empty addresses are still filtered out"
);
}
_ => panic!("expected diff prestate frame"),
}
}
}