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}