use alloy_primitives::{Address, Bytes, U256};
use edb_common::{
types::{ExecutionFrameId, Trace},
EdbContext, OpcodeTr,
};
use revm::{
bytecode::opcode::OpCode,
context::{ContextTr, LocalContextTr},
database::CacheDB,
interpreter::{
interpreter_types::{InputsTr, Jumps},
CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter,
},
state::TransientStorage,
Database, DatabaseCommit, DatabaseRef, Inspector,
};
use serde::{Deserialize, Serialize};
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
sync::Arc,
};
use tracing::error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpcodeSnapshot<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
pub pc: usize,
pub target_address: Address,
pub bytecode_address: Address,
pub opcode: u8,
pub memory: Arc<Vec<u8>>,
pub stack: Vec<U256>,
pub calldata: Arc<Bytes>,
pub database: Arc<CacheDB<DB>>,
pub transient_storage: Arc<TransientStorage>,
}
#[derive(Debug, Clone)]
pub struct OpcodeSnapshots<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
inner: HashMap<ExecutionFrameId, Vec<OpcodeSnapshot<DB>>>,
}
impl<DB> Default for OpcodeSnapshots<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
fn default() -> Self {
Self { inner: HashMap::new() }
}
}
impl<DB> Deref for OpcodeSnapshots<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
type Target = HashMap<ExecutionFrameId, Vec<OpcodeSnapshot<DB>>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<DB> DerefMut for OpcodeSnapshots<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
#[derive(Debug, Clone)]
struct FrameState {
last_memory: Arc<Vec<u8>>,
last_calldata: Arc<Bytes>,
}
#[derive(Debug)]
pub struct OpcodeSnapshotInspector<'a, DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
trace: &'a Trace,
pub snapshots: OpcodeSnapshots<DB>,
pub excluded_addresses: HashSet<Address>,
frame_stack: Vec<ExecutionFrameId>,
current_trace_id: usize,
frame_states: HashMap<ExecutionFrameId, FrameState>,
database: Arc<CacheDB<DB>>,
transition_storage: Arc<TransientStorage>,
last_opcode: Option<OpCode>,
}
impl<'a, DB> OpcodeSnapshotInspector<'a, DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
pub fn new(ctx: &EdbContext<DB>, trace: &'a Trace) -> Self {
Self {
trace,
snapshots: OpcodeSnapshots::<DB>::default(),
excluded_addresses: HashSet::new(),
frame_stack: Vec::new(),
current_trace_id: 0,
frame_states: HashMap::new(),
database: Arc::new(ctx.db().clone()),
transition_storage: Arc::new(TransientStorage::default()),
last_opcode: None,
}
}
pub fn with_excluded_addresses(&mut self, excluded_addresses: HashSet<Address>) {
self.excluded_addresses = excluded_addresses;
}
pub fn into_snapshots(self) -> OpcodeSnapshots<DB> {
self.snapshots
}
pub fn exclude_address(&mut self, address: Address) {
self.excluded_addresses.insert(address);
}
fn current_frame_id(&self) -> Option<ExecutionFrameId> {
self.frame_stack.last().copied()
}
fn should_record(&self, address: Address) -> bool {
!self.excluded_addresses.contains(&address)
}
fn update_storage(&mut self, _interp: &Interpreter, ctx: &mut EdbContext<DB>) {
let Some(last_opcode) = self.last_opcode else { return };
if last_opcode.modifies_evm_state() {
let mut inner = ctx.journal().to_inner();
let changes = inner.finalize();
let mut snap = ctx.db().clone();
snap.commit(changes);
self.database = Arc::new(snap);
}
if last_opcode.modifies_transient_storage() {
let transient_storage = ctx.journal().transient_storage.clone();
self.transition_storage = Arc::new(transient_storage);
}
}
fn record_snapshot(&mut self, interp: &Interpreter, ctx: &mut EdbContext<DB>) {
let opcode = unsafe { OpCode::new_unchecked(interp.bytecode.opcode()) };
self.last_opcode = Some(opcode);
let Some(frame_id) = self.current_frame_id() else {
return;
};
let contract_address =
interp.input.bytecode_address().cloned().unwrap_or(interp.input.target_address());
if !self.should_record(contract_address) {
return;
}
let address = interp.input.target_address();
let frame_state = self.frame_states.get(&frame_id);
let memory = if let Some(state) = frame_state {
let mem_ref = interp.memory.borrow();
let current_memory = mem_ref.context_memory();
if current_memory.len() == state.last_memory.len()
&& &*current_memory == state.last_memory.as_slice()
{
state.last_memory.clone()
} else {
Arc::new(current_memory.to_vec())
}
} else {
Arc::new(interp.memory.borrow().context_memory().to_vec())
};
let calldata = if let Some(state) = frame_state {
state.last_calldata.clone()
} else {
match interp.input.input() {
revm::interpreter::CallInput::SharedBuffer(range) => Arc::new(
ctx.local()
.shared_memory_buffer_slice(range.clone())
.map(|slice| Bytes::from(slice.to_vec()))
.unwrap_or_else(Bytes::new),
),
revm::interpreter::CallInput::Bytes(bytes) => Arc::new(bytes.clone()),
}
};
let entry = self.trace.get(frame_id.trace_entry_id());
let snapshot = OpcodeSnapshot {
pc: interp.bytecode.pc(),
bytecode_address: entry.map(|t| t.code_address).unwrap_or(address),
target_address: entry.map(|t| t.target).unwrap_or(address),
opcode: opcode.get(),
memory: memory.clone(),
stack: interp.stack.data().clone(),
calldata: calldata.clone(),
database: self.database.clone(),
transient_storage: self.transition_storage.clone(),
};
self.snapshots.entry(frame_id).or_default().push(snapshot);
self.frame_states
.insert(frame_id, FrameState { last_memory: memory, last_calldata: calldata });
}
fn push_frame(&mut self, trace_id: usize) {
let frame_id = ExecutionFrameId::new(trace_id, 0);
self.frame_stack.push(frame_id);
self.snapshots.entry(frame_id).or_default();
}
fn pop_frame(&mut self) -> Option<ExecutionFrameId> {
if let Some(frame_id) = self.frame_stack.pop() {
self.frame_states.remove(&frame_id);
if let Some(parent_frame_id) = self.frame_stack.last_mut() {
parent_frame_id.increment_re_entry();
}
Some(frame_id)
} else {
None
}
}
pub fn get_frame_snapshots(
&self,
frame_id: ExecutionFrameId,
) -> Option<&Vec<OpcodeSnapshot<DB>>> {
self.snapshots.get(&frame_id)
}
pub fn get_recorded_frames(&self) -> Vec<ExecutionFrameId> {
self.snapshots.keys().copied().collect()
}
pub fn clear(&mut self) {
self.snapshots.clear();
self.frame_stack.clear();
self.frame_states.clear();
self.current_trace_id = 0;
}
}
impl<'a, DB> Inspector<EdbContext<DB>> for OpcodeSnapshotInspector<'a, DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
fn step(&mut self, interp: &mut Interpreter, context: &mut EdbContext<DB>) {
self.record_snapshot(interp, context);
}
fn step_end(&mut self, interp: &mut Interpreter, context: &mut EdbContext<DB>) {
self.update_storage(interp, context);
}
fn call(
&mut self,
_context: &mut EdbContext<DB>,
_inputs: &mut CallInputs,
) -> Option<CallOutcome> {
self.push_frame(self.current_trace_id);
self.current_trace_id += 1;
None
}
fn call_end(
&mut self,
_context: &mut EdbContext<DB>,
_inputs: &CallInputs,
outcome: &mut CallOutcome,
) {
let Some(frame_id) = self.pop_frame() else { return };
let Some(entry) = self.trace.get(frame_id.trace_entry_id()) else { return };
if entry.result != Some(outcome.into()) {
error!(
"Call outcome mismatch in frame {:?}: expected {:?}, got {:?}",
frame_id, entry.result, outcome
);
}
}
fn create(
&mut self,
_context: &mut EdbContext<DB>,
_inputs: &mut CreateInputs,
) -> Option<CreateOutcome> {
self.push_frame(self.current_trace_id);
self.current_trace_id += 1;
None
}
fn create_end(
&mut self,
_context: &mut EdbContext<DB>,
_inputs: &CreateInputs,
outcome: &mut CreateOutcome,
) {
let Some(frame_id) = self.pop_frame() else { return };
let Some(entry) = self.trace.get(frame_id.trace_entry_id()) else { return };
if entry.result != Some(outcome.into()) {
error!(
"Create outcome mismatch in frame {:?}: expected {:?}, got {:?}",
frame_id, entry.result, outcome
);
}
}
}
impl<DB> OpcodeSnapshots<DB>
where
DB: Database + DatabaseCommit + DatabaseRef + Clone,
<CacheDB<DB> as Database>::Error: Clone,
<DB as Database>::Error: Clone,
{
pub fn print_summary(&self) {
println!(
"\n\x1b[36m╔══════════════════════════════════════════════════════════════════╗\x1b[0m"
);
println!(
"\x1b[36m║ OPCODE SNAPSHOT INSPECTOR SUMMARY ║\x1b[0m"
);
println!(
"\x1b[36m╚══════════════════════════════════════════════════════════════════╝\x1b[0m\n"
);
let total_frames = self.len();
let total_snapshots: usize = self.values().map(|v| v.len()).sum();
println!("\x1b[33m📊 Overall Statistics:\x1b[0m");
println!(" Total frames recorded: \x1b[32m{total_frames}\x1b[0m");
println!(" Total snapshots recorded: \x1b[32m{total_snapshots}\x1b[0m");
if self.is_empty() {
println!("\n\x1b[90m No opcode snapshots were recorded.\x1b[0m");
return;
}
let mut total_memory_instances = 0;
let mut unique_memory_instances = HashSet::new();
let mut total_calldata_instances = 0;
let mut unique_calldata_instances = HashSet::new();
for snapshots in self.values() {
for snapshot in snapshots {
total_memory_instances += 1;
unique_memory_instances.insert(Arc::as_ptr(&snapshot.memory) as usize);
total_calldata_instances += 1;
unique_calldata_instances.insert(Arc::as_ptr(&snapshot.calldata) as usize);
}
}
let memory_sharing_ratio = if total_memory_instances > 0 {
(total_memory_instances - unique_memory_instances.len()) as f64
/ total_memory_instances as f64
* 100.0
} else {
0.0
};
let calldata_sharing_ratio = if total_calldata_instances > 0 {
(total_calldata_instances - unique_calldata_instances.len()) as f64
/ total_calldata_instances as f64
* 100.0
} else {
0.0
};
println!("\n\x1b[33m💾 Memory Optimization:\x1b[0m");
println!(" Memory - Unique instances: \x1b[32m{}\x1b[0m / Total refs: \x1b[32m{}\x1b[0m (Sharing: \x1b[32m{:.1}%\x1b[0m)",
unique_memory_instances.len(), total_memory_instances, memory_sharing_ratio);
println!(" Calldata - Unique instances: \x1b[32m{}\x1b[0m / Total refs: \x1b[32m{}\x1b[0m (Sharing: \x1b[32m{:.1}%\x1b[0m)",
unique_calldata_instances.len(), total_calldata_instances, calldata_sharing_ratio);
println!("\n\x1b[33m📋 Frame Details:\x1b[0m");
println!(
"\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
);
let mut sorted_frames: Vec<_> = self.iter().collect();
sorted_frames
.sort_by_key(|(frame_id, _)| (frame_id.trace_entry_id(), frame_id.re_entry_count()));
for (frame_id, snapshots) in sorted_frames {
let color = if snapshots.is_empty() {
"\x1b[90m" } else if snapshots.len() < 10 {
"\x1b[32m" } else if snapshots.len() < 100 {
"\x1b[33m" } else {
"\x1b[31m" };
println!(
"\n {}Frame {}\x1b[0m (trace.{}, re-entry {})",
color,
frame_id,
frame_id.trace_entry_id(),
frame_id.re_entry_count()
);
println!(" └─ Snapshots: \x1b[36m{}\x1b[0m", snapshots.len());
if !snapshots.is_empty() {
let preview_count = 3.min(snapshots.len());
println!(" \x1b[90mFirst {preview_count} snapshots:\x1b[0m");
for (i, snapshot) in snapshots.iter().take(preview_count).enumerate() {
self.print_snapshot_line(i, snapshot, " ");
}
if snapshots.len() > preview_count * 2 {
println!(
" \x1b[90m... {} more snapshots ...\x1b[0m",
snapshots.len() - preview_count * 2
);
println!(" \x1b[90mLast {preview_count} snapshots:\x1b[0m");
let start_idx = snapshots.len() - preview_count;
for (i, snapshot) in snapshots.iter().skip(start_idx).enumerate() {
self.print_snapshot_line(start_idx + i, snapshot, " ");
}
} else if snapshots.len() > preview_count {
for (i, snapshot) in snapshots.iter().skip(preview_count).enumerate() {
self.print_snapshot_line(preview_count + i, snapshot, " ");
}
}
let total_memory: usize = snapshots.iter().map(|s| s.memory.len()).sum();
let avg_stack_depth: f64 = snapshots.iter().map(|s| s.stack.len()).sum::<usize>()
as f64
/ snapshots.len() as f64;
println!(" \x1b[90m├─ Avg stack depth: {avg_stack_depth:.1}\x1b[0m");
println!(" \x1b[90m└─ Total memory used: {total_memory} bytes\x1b[0m");
}
}
println!(
"\n\x1b[90m─────────────────────────────────────────────────────────────────\x1b[0m"
);
}
fn print_snapshot_line(&self, index: usize, snapshot: &OpcodeSnapshot<DB>, indent: &str) {
let opcode = unsafe { OpCode::new_unchecked(snapshot.opcode) };
let opcode_str = opcode.as_str().to_string();
#[allow(deprecated)]
let addr_short = format!("{:?}", snapshot.bytecode_address);
let addr_display = if addr_short.len() > 10 {
format!("{}...{}", &addr_short[0..6], &addr_short[addr_short.len() - 4..])
} else {
addr_short
};
println!(
"{} [{:4}] PC={:5} \x1b[94m{:18}\x1b[0m @ \x1b[37m{}\x1b[0m | Stack:{:2} Mem:{:6}B",
indent,
index,
snapshot.pc,
opcode_str,
addr_display,
snapshot.stack.len(),
snapshot.memory.len()
);
}
}