edb_engine/inspector/
call_tracer.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Call tracer for collecting complete execution trace during transaction replay
18//!
19//! This inspector captures the complete call trace including call stack, creation events,
20//! and execution flow. The trace can be replayed later to determine execution paths
21//! without needing to re-examine transaction inputs/outputs.
22
23use alloy_primitives::{Address, Log, U256};
24use edb_common::types::{CallResult, Trace, TraceEntry};
25use revm::{
26    context::ContextTr,
27    interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter},
28    Inspector,
29};
30use std::{collections::HashMap, ops::Deref};
31use tracing::{debug, error};
32
33/// Result of transaction replay with call trace
34#[derive(Debug)]
35pub struct TraceReplayResult {
36    /// All addresses visited during execution
37    pub visited_addresses: HashMap<Address, bool>,
38    /// Complete execution trace with call/create details
39    pub execution_trace: Trace,
40}
41
42/// Complete call tracer that captures execution flow
43#[derive(Debug, Default)]
44pub struct CallTracer {
45    /// Sequential list of all calls/creates in execution order
46    pub trace: Trace,
47    /// Map of visited addresses to whether they were deployed in this transaction
48    pub visited_addresses: HashMap<Address, bool>,
49    /// Stack to track call indices for proper nesting
50    call_stack: Vec<usize>,
51}
52
53impl CallTracer {
54    /// Create a new call tracer
55    pub fn new() -> Self {
56        Self { trace: Trace::default(), visited_addresses: HashMap::new(), call_stack: Vec::new() }
57    }
58
59    /// Get all visited addresses
60    pub fn visited_addresses(&self) -> &HashMap<Address, bool> {
61        &self.visited_addresses
62    }
63
64    /// Get the complete execution trace
65    pub fn execution_trace(&self) -> &Trace {
66        &self.trace
67    }
68
69    /// Convert the call tracer into a replay result
70    pub fn into_replay_result(self) -> TraceReplayResult {
71        TraceReplayResult { visited_addresses: self.visited_addresses, execution_trace: self.trace }
72    }
73
74    /// Add an address to the visited set
75    fn mark_address_visited(&mut self, address: Address, deployed: bool) {
76        self.visited_addresses
77            .entry(address)
78            .and_modify(|existing| *existing |= deployed)
79            .or_insert(deployed);
80    }
81}
82
83impl<CTX: ContextTr> Inspector<CTX> for CallTracer {
84    fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
85        let Some(entry) = self.trace.last_mut() else {
86            debug!("Trace is empty, cannot step");
87            return;
88        };
89
90        if entry.bytecode.is_some() {
91            // We already update the bytecode for the current entry
92            return;
93        }
94
95        entry.bytecode = Some(interp.bytecode.bytes());
96    }
97
98    fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
99        let call_type = inputs.into();
100        let target = inputs.target_address;
101        let code_address = inputs.bytecode_address;
102        let caller = inputs.caller;
103
104        // Mark addresses as visited
105        self.mark_address_visited(target, false);
106        self.mark_address_visited(code_address, false);
107        self.mark_address_visited(caller, false);
108
109        // Determine the parent ID from the current call stack
110        let parent_id = self.call_stack.last().copied();
111        let trace_id = self.trace.len();
112
113        // Create trace entry
114        let trace_entry = TraceEntry {
115            id: trace_id,
116            parent_id,
117            depth: self.call_stack.len(),
118            call_type,
119            caller,
120            target,
121            code_address,
122            input: inputs.input.bytes(context),
123            value: inputs.transfer_value().unwrap_or(U256::ZERO),
124            result: None,        // Will be filled in call_end
125            events: vec![],      // Will be filled in log
126            self_destruct: None, // Will be filled in self_destruct
127            created_contract: false,
128            create_scheme: None,
129            bytecode: None,          // Will be set in step
130            target_label: None,      // Will be set in post-analysis
131            first_snapshot_id: None, // Will be set in post-analysis
132        };
133
134        // Add to trace and update stack
135        self.trace.push(trace_entry);
136        self.call_stack.push(trace_id);
137
138        None // Continue with normal execution
139    }
140
141    fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) {
142        // Pop from call stack and update result
143        let Some(trace_index) = self.call_stack.pop() else {
144            error!("Call stack underflow - no matching call entry found");
145            return;
146        };
147
148        let Some(trace_entry) = self.trace.get_mut(trace_index) else {
149            error!("Call stack entry not found");
150            return;
151        };
152
153        trace_entry.result = Some(outcome.into());
154
155        let target = inputs.target_address;
156        let code_address = inputs.bytecode_address;
157        let caller = inputs.caller;
158        if trace_entry.target != target
159            || trace_entry.code_address != code_address
160            || trace_entry.caller != caller
161        {
162            error!("Call stack entry mismatch");
163        }
164    }
165
166    fn create(&mut self, _context: &mut CTX, inputs: &mut CreateInputs) -> Option<CreateOutcome> {
167        let call_type = inputs.into();
168        let caller = inputs.caller;
169
170        // Mark addresses
171        self.mark_address_visited(caller, false);
172
173        // Determine the parent ID from the current call stack
174        let parent_id = self.call_stack.last().copied();
175        let trace_id = self.trace.len();
176
177        // Create trace entry
178        let trace_entry = TraceEntry {
179            id: trace_id,
180            parent_id,
181            depth: self.call_stack.len(),
182            call_type,
183            caller,
184            target: Address::ZERO,       // Target is not known yet
185            code_address: Address::ZERO, // Code address is not known yet
186            input: inputs.init_code.clone(),
187            value: inputs.value,
188            result: None,            // Will be filled in create_end
189            events: vec![],          // Will be filled in log
190            self_destruct: None,     // Will be filled in self_destruct
191            created_contract: false, // Will be updated in create_end
192            create_scheme: Some(inputs.scheme),
193            bytecode: None,          // Will be set in step
194            target_label: None,      // Will be set in post-analysis
195            first_snapshot_id: None, // Will be set in post-analysis
196        };
197
198        // Add to trace and update stack
199        self.trace.push(trace_entry);
200        self.call_stack.push(trace_id);
201
202        None // Continue with normal execution
203    }
204
205    fn create_end(
206        &mut self,
207        _context: &mut CTX,
208        inputs: &CreateInputs,
209        outcome: &mut CreateOutcome,
210    ) {
211        // Pop from call stack and update result
212        let Some(trace_index) = self.call_stack.pop() else {
213            error!("Call stack underflow - no matching create entry found");
214            return;
215        };
216
217        let Some(trace_entry) = self.trace.get_mut(trace_index) else {
218            error!("Trace entry not found");
219            return;
220        };
221
222        // Check caller consistence first, and no need to return
223        let caller = inputs.caller;
224        if trace_entry.caller != caller {
225            error!("Create stack entry mismatch");
226        }
227
228        trace_entry.result = Some(outcome.into());
229
230        if matches!(trace_entry.result, Some(CallResult::Revert { .. })) {
231            debug!("Creation failed");
232            return;
233        }
234
235        let Some(created_address) = outcome.address else {
236            error!("Create outcome did not provide created address");
237            return;
238        };
239
240        trace_entry.target = created_address;
241        trace_entry.code_address = created_address;
242        trace_entry.created_contract = true;
243
244        let created_address_for_marking = trace_entry.target;
245        // Mark address after trace_entry is no longer borrowed
246        let _ = trace_entry;
247
248        self.mark_address_visited(created_address_for_marking, true);
249    }
250
251    fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) {
252        // Mark both addresses as visited
253        self.mark_address_visited(contract, false);
254        self.mark_address_visited(target, false);
255
256        let Some(entry) = self.trace.last_mut() else {
257            error!("Trace is empty, cannot step");
258            return;
259        };
260
261        if entry.target != contract {
262            error!("Self-destruct entry mismatch");
263            return;
264        }
265        entry.self_destruct = Some((target, value));
266    }
267
268    fn log(&mut self, _interp: &mut Interpreter, _context: &mut CTX, log: Log) {
269        let Some(entry) = self.trace.last_mut() else {
270            error!("Trace is empty, cannot log");
271            return;
272        };
273
274        entry.events.push(log.deref().clone());
275    }
276}