edb_engine/context.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//! Engine context management and EVM instantiation for debugging.
18//!
19//! This module provides the core [`EngineContext`] struct that consolidates all debugging
20//! data and state needed for time travel debugging and expression evaluation. The context
21//! serves as the primary data structure passed to the JSON-RPC server and contains all
22//! analysis results, snapshots, artifacts, and execution state.
23//!
24//! # Core Components
25//!
26//! ## EngineContext
27//! The [`EngineContext`] is the central data structure that encapsulates:
28//! - **Fork Information**: Network and block context for the debugging session
29//! - **EVM Environment**: Configuration, block, and transaction environments
30//! - **Snapshots**: Merged opcode-level and hook-based execution snapshots
31//! - **Artifacts**: Original and recompiled contract artifacts with source code
32//! - **Analysis Results**: Instrumentation points and debugging metadata
33//! - **Execution Trace**: Call hierarchy and frame structure
34//!
35//! ## EVM Instantiation
36//! The context provides methods to create derived EVMs for expression evaluation:
37//! - **Snapshot-specific EVMs**: Create EVM instances at any execution point
38//! - **Transaction replay**: Send mock transactions in derived state
39//! - **Function calls**: Invoke contract functions for expression evaluation
40//!
41//! # Workflow Integration
42//!
43//! 1. **Context Building**: Constructed after analysis and snapshot collection
44//! 2. **Finalization**: Processes traces and pre-evaluates state variables
45//! 3. **Server Integration**: Passed to JSON-RPC server for debugging API
46//! 4. **Expression Evaluation**: Used to create derived EVMs for real-time evaluation
47//!
48//! # Thread Safety
49//!
50//! The [`EngineContext`] is designed to be thread-safe and can be shared across
51//! multiple debugging clients through Arc wrapping. All database operations
52//! use read-only snapshots to ensure debugging session integrity.
53
54use std::{
55 collections::{HashMap, HashSet},
56 sync::Arc,
57};
58
59use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt};
60use alloy_json_abi::Function;
61use alloy_primitives::{Address, Bytes, TxHash, U256};
62use edb_common::{
63 disable_nonce_check, relax_evm_context_constraints, relax_evm_tx_constraints,
64 types::{parse_callable_abi_entries, Trace},
65 DerivedContext, ForkInfo,
66};
67use eyre::{eyre, Result};
68use indicatif::ProgressBar;
69use once_cell::sync::OnceCell;
70use revm::{
71 context::{result::ExecutionResult, tx::TxEnvBuilder, BlockEnv, CfgEnv, TxEnv},
72 database::CacheDB,
73 Context, Database, DatabaseCommit, DatabaseRef, ExecuteEvm, MainBuilder, MainContext,
74 MainnetEvm,
75};
76use serde::{Deserialize, Serialize};
77use tracing::error;
78
79use crate::{analysis::AnalysisResult, Artifact, SnapshotDetail, Snapshots};
80
81/// Complete debugging context containing all analysis results and state snapshots
82///
83/// This struct encapsulates all the data produced during the debugging workflow,
84/// including the original transaction context, collected snapshots, analyzed source code,
85/// and recompiled artifacts. It serves as the primary data structure passed to the
86/// JSON-RPC server for time travel debugging.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct EngineContext<DB>
89where
90 DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
91 <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
92 <DB as Database>::Error: Clone + Send + Sync,
93{
94 /// Forked information
95 pub fork_info: ForkInfo,
96 /// Configuration environment for the EVM
97 pub cfg: CfgEnv,
98 /// Block environment for the target block
99 pub block: BlockEnv,
100 /// Transaction environment for the target transaction
101 pub tx: TxEnv,
102 /// Transaction hash for the target transaction
103 pub tx_hash: TxHash,
104 /// Merged snapshots from both opcode-level and hook-based collection
105 pub snapshots: Snapshots<DB>,
106 /// Original contract artifacts with source code and metadata
107 pub artifacts: HashMap<Address, Artifact>,
108 /// Recompiled artifacts with instrumented source code
109 pub recompiled_artifacts: HashMap<Address, Artifact>,
110 /// Analysis results identifying instrumentation points
111 pub analysis_results: HashMap<Address, AnalysisResult>,
112 /// Execution trace showing call hierarchy and frame structure
113 pub trace: Trace,
114 /// Relation between target addresses and their (delegated) code addresses
115 #[serde(skip)]
116 address_code_address_map: OnceCell<HashMap<Address, HashSet<Address>>>,
117}
118
119impl<DB> EngineContext<DB>
120where
121 DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
122 <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
123 <DB as Database>::Error: Clone + Send + Sync,
124{
125 /// Build a new EngineContext with all debugging data.
126 ///
127 /// This constructor consolidates all the debugging data collected during the analysis
128 /// and snapshot collection phases into a unified context for debugging operations.
129 ///
130 /// # Arguments
131 ///
132 /// * `fork_info` - Network and fork information for the debugging session
133 /// * `cfg` - EVM configuration environment
134 /// * `block` - Block environment for the target block
135 /// * `tx` - Transaction environment for the target transaction
136 /// * `tx_hash` - Hash of the target transaction
137 /// * `snapshots` - Merged snapshots from opcode and hook collection
138 /// * `artifacts` - Original contract artifacts with source code
139 /// * `recompiled_artifacts` - Recompiled artifacts with instrumentation
140 /// * `analysis_results` - Analysis results identifying instrumentation points
141 /// * `trace` - Execution trace showing call hierarchy
142 ///
143 /// # Returns
144 ///
145 /// Returns a finalized [`EngineContext`] ready for debugging operations.
146 #[allow(clippy::too_many_arguments)]
147 pub fn build(
148 fork_info: ForkInfo,
149 cfg: CfgEnv,
150 block: BlockEnv,
151 tx: TxEnv,
152 tx_hash: TxHash,
153 snapshots: Snapshots<DB>,
154 artifacts: HashMap<Address, Artifact>,
155 recompiled_artifacts: HashMap<Address, Artifact>,
156 analysis_results: HashMap<Address, AnalysisResult>,
157 trace: Trace,
158 ) -> Result<Self> {
159 let mut context = Self {
160 fork_info,
161 cfg,
162 block,
163 tx,
164 tx_hash,
165 snapshots,
166 artifacts,
167 recompiled_artifacts,
168 analysis_results,
169 trace,
170 address_code_address_map: OnceCell::new(),
171 };
172
173 // Finalize the context to populate derived fields
174 context.finalize()?;
175 Ok(context)
176 }
177
178 /// Finalize the EngineContext by processing traces and snapshots.
179 ///
180 /// This method performs post-processing on the collected debugging data:
181 /// 1. Links trace entries with their corresponding snapshot IDs
182 /// 2. Pre-evaluates state variables for all hook-based snapshots
183 /// 3. Populates derived mappings for efficient lookups
184 fn finalize(&mut self) -> Result<()> {
185 self.finalize_trace()?;
186 self.finalize_snapshots()?;
187
188 Ok(())
189 }
190
191 /// Finalize the trace by linking trace entries with their corresponding snapshot IDs.
192 ///
193 /// This method processes the execution trace to establish the relationship between
194 /// trace entries and snapshots, enabling efficient navigation during debugging.
195 fn finalize_trace(&mut self) -> Result<()> {
196 for entry in &mut self.trace {
197 let trace_id = entry.id;
198
199 // We first update the snapshot id
200 for (snapshot_id, (frame_id, _)) in self.snapshots.iter().enumerate() {
201 if frame_id.trace_entry_id() == trace_id {
202 entry.first_snapshot_id = Some(snapshot_id);
203 break;
204 }
205 }
206 }
207
208 Ok(())
209 }
210
211 /// Finalize snapshots by pre-evaluating state variables.
212 ///
213 /// This method processes all hook-based snapshots to pre-evaluate their state
214 /// variables, making them immediately available for expression evaluation.
215 /// This optimization reduces latency during debugging sessions.
216 fn finalize_snapshots(&mut self) -> Result<()> {
217 let tx_hash = self.tx_hash;
218
219 // Actually execute each transaction with revm
220 let console_bar = Arc::new(ProgressBar::new(self.snapshots.len() as u64));
221 let template = format!("{{spinner:.green}} 🔮 Finalizing steps for {} [{{bar:40.cyan/blue}}] {{pos:>3}}/{{len:3}} ⛽ {{msg}}", &tx_hash.to_string()[2..10]);
222 console_bar.set_style(
223 indicatif::ProgressStyle::with_template(&template)?
224 .progress_chars("🟩🟦⬜")
225 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
226 );
227
228 let mut results: HashMap<usize, _> = HashMap::new();
229 for (snapshot_id, (_, snapshot)) in self.snapshots.iter().enumerate() {
230 if snapshot.is_opcode() {
231 console_bar.set_message(format!("Analyzing step {snapshot_id} w/o source code"));
232 console_bar.inc(1);
233 continue;
234 } else {
235 console_bar.set_message(format!("Analyzing step {snapshot_id} with source code"));
236 console_bar.inc(1);
237 }
238
239 let code_address = snapshot.bytecode_address();
240 let Some(contract) =
241 self.recompiled_artifacts.get(&code_address).and_then(|a| a.contract())
242 else {
243 return Err(eyre!("No contract found for address {}", code_address));
244 };
245
246 let mut states = HashMap::new();
247 for state_variable in parse_callable_abi_entries(contract)
248 .into_iter()
249 .filter(|v| v.is_state_variable() && v.inputs.is_empty())
250 {
251 match self.call_in_derived_evm(
252 snapshot_id,
253 snapshot.target_address(),
254 &state_variable.abi,
255 &[],
256 None,
257 ) {
258 Ok(value) => {
259 states.insert(state_variable.name.clone(), Some(Arc::new(value.into())));
260 }
261 Err(e) => {
262 error!(id=?snapshot_id, "Failed to call state variable: {} ({})", state_variable, e);
263 states.insert(state_variable.name.clone(), None);
264 }
265 }
266 }
267
268 results.insert(snapshot_id, states);
269 }
270
271 for (snapshot_id, states) in results.into_iter() {
272 if let Some((_, snapshot)) = self.snapshots.get_mut(snapshot_id) {
273 if let SnapshotDetail::Hook(ref mut hook_detail) = snapshot.detail_mut() {
274 hook_detail.state_variables = states;
275 }
276 }
277 }
278
279 console_bar.finish_with_message(format!(
280 "✨ Ready! All {} steps analyzed and finalized.",
281 self.snapshots.len()
282 ));
283
284 Ok(())
285 }
286}
287
288// Context query methods for debugging operations
289impl<DB> EngineContext<DB>
290where
291 DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
292 <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
293 <DB as Database>::Error: Clone + Send + Sync,
294{
295 /// Get the bytecode address for a snapshot.
296 ///
297 /// Returns the address where the executing bytecode is stored, which may differ
298 /// from the target address in cases of delegatecall or proxy contracts.
299 pub fn get_bytecode_address(&self, snapshot_id: usize) -> Option<Address> {
300 let (frame_id, _) = self.snapshots.get(snapshot_id)?;
301 self.trace.get(frame_id.trace_entry_id()).map(|entry| entry.code_address)
302 }
303
304 /// Get the target address for a snapshot.
305 ///
306 /// Returns the address that was the target of the call, which is the address
307 /// receiving the call in the current execution frame.
308 pub fn get_target_address(&self, snapshot_id: usize) -> Option<Address> {
309 let (frame_id, _) = self.snapshots.get(snapshot_id)?;
310 self.trace.get(frame_id.trace_entry_id()).map(|entry| entry.target)
311 }
312
313 /// Check if one trace entry is the parent of another.
314 ///
315 /// This method determines the parent-child relationship between trace entries,
316 /// useful for understanding call hierarchy during debugging.
317 pub fn is_parent_trace(&self, parent_id: usize, child_id: usize) -> bool {
318 match self.trace.get(child_id) {
319 Some(child_entry) => child_entry.parent_id == Some(parent_id),
320 None => false,
321 }
322 }
323
324 /// Get the address to code address mapping.
325 ///
326 /// Returns a cached mapping from target addresses to all code addresses that
327 /// have been executed for each target. This is useful for understanding
328 /// proxy patterns and delegatecall relationships.
329 pub fn address_code_address_map(&self) -> &HashMap<Address, HashSet<Address>> {
330 self.address_code_address_map.get_or_init(|| {
331 let mut map: HashMap<Address, HashSet<Address>> = HashMap::new();
332 for entry in &self.trace {
333 map.entry(entry.target).or_default().insert(entry.code_address);
334 }
335 map
336 })
337 }
338}
339
340// EVM creation and expression evaluation methods
341impl<DB> EngineContext<DB>
342where
343 DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
344 <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
345 <DB as Database>::Error: Clone + Send + Sync,
346{
347 /// Create a derived EVM instance for a specific snapshot.
348 ///
349 /// This method creates a new EVM instance using the database state from the
350 /// specified snapshot. The resulting EVM can be used for expression evaluation
351 /// and function calls without affecting the original debugging state.
352 ///
353 /// # Arguments
354 ///
355 /// * `snapshot_id` - The snapshot ID to create the EVM for
356 ///
357 /// # Returns
358 ///
359 /// Returns a configured EVM instance or None if the snapshot doesn't exist.
360 pub fn create_evm_for_snapshot(
361 &self,
362 snapshot_id: usize,
363 ) -> Option<MainnetEvm<DerivedContext<DB>>> {
364 let (_, snapshot) = self.snapshots.get(snapshot_id)?;
365
366 let db = CacheDB::new(CacheDB::new(snapshot.db()));
367 let cfg = self.cfg.clone();
368 let block = self.block.clone();
369
370 let mut ctx = Context::mainnet().with_db(db).with_cfg(cfg).with_block(block);
371 relax_evm_context_constraints(&mut ctx);
372 disable_nonce_check(&mut ctx);
373
374 Some(ctx.build_mainnet())
375 }
376
377 /// Send a mock transaction in a derived EVM.
378 ///
379 /// This method executes a transaction in the EVM state at the specified snapshot
380 /// without affecting the original debugging state. Used for expression evaluation
381 /// that requires transaction execution.
382 ///
383 /// # Arguments
384 ///
385 /// * `snapshot_id` - The snapshot ID to use as the base state
386 /// * `to` - The target address for the transaction
387 /// * `data` - The transaction data (call data)
388 /// * `value` - The value to send with the transaction
389 ///
390 /// # Returns
391 ///
392 /// Returns the execution result or an error if the transaction fails.
393 pub fn send_transaction_in_derived_evm(
394 &self,
395 snapshot_id: usize,
396 to: Address,
397 data: &[u8],
398 value: U256,
399 ) -> Result<ExecutionResult> {
400 let mut evm = self
401 .create_evm_for_snapshot(snapshot_id)
402 .ok_or(eyre!("No EVM found at snapshot {}", snapshot_id))?;
403
404 let mut tx_env = TxEnvBuilder::new()
405 .caller(self.tx.caller)
406 .call(to)
407 .value(value)
408 .data(Bytes::copy_from_slice(data))
409 .build_fill();
410 relax_evm_tx_constraints(&mut tx_env);
411
412 evm.transact_one(tx_env).map_err(|e| eyre!(e.to_string()))
413 }
414
415 /// Invoke a contract function call in a derived EVM.
416 ///
417 /// This method calls a specific contract function in the EVM state at the
418 /// specified snapshot. It handles ABI encoding/decoding automatically and
419 /// returns the decoded result.
420 ///
421 /// # Arguments
422 ///
423 /// * `snapshot_id` - The snapshot ID to use as the base state
424 /// * `to` - The contract address to call
425 /// * `function` - The ABI function definition
426 /// * `args` - The function arguments
427 /// * `value` - Optional value to send with the call
428 ///
429 /// # Returns
430 ///
431 /// Returns the decoded function result or an error if the call fails.
432 pub fn call_in_derived_evm(
433 &self,
434 snapshot_id: usize,
435 to: Address,
436 function: &Function,
437 args: &[DynSolValue],
438 value: Option<U256>,
439 ) -> Result<DynSolValue> {
440 let data = function.abi_encode_input(args).map_err(|e| eyre!(e.to_string()))?;
441 let value = value.unwrap_or_default();
442
443 let result = self.send_transaction_in_derived_evm(snapshot_id, to, &data, value)?;
444
445 match result {
446 ExecutionResult::Success { output, .. } => {
447 let decoded =
448 function.abi_decode_output(output.data()).map_err(|e| eyre!(e.to_string()))?;
449 if decoded.len() == 1 {
450 Ok(decoded.into_iter().next().unwrap())
451 } else {
452 Ok(DynSolValue::Tuple(decoded))
453 }
454 }
455 ExecutionResult::Revert { output, .. } => {
456 Err(eyre!("Call reverted with output: 0x{}", hex::encode(output)))
457 }
458 ExecutionResult::Halt { reason, .. } => {
459 Err(eyre!("Call halted with reason: {:?}", reason))
460 }
461 }
462 }
463}