edb_engine/snapshot/
mod.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//! Unified snapshot management for time travel debugging
18//!
19//! This module provides a unified interface for managing both opcode-level and hook-based
20//! snapshots. It merges the two different snapshot types into a coherent structure that
21//! maintains execution order and frame relationships for effective debugging.
22//!
23//! The main structure `Snapshots` combines:
24//! - Opcode snapshots: Fine-grained instruction-level state captures
25//! - Hook snapshots: Strategic breakpoint-based state captures
26//!
27//! The merging process prioritizes hook snapshots when available, falling back to
28//! opcode snapshots for frames without hooks. This provides a comprehensive view
29//! of execution state across the entire transaction.
30
31mod analysis;
32mod pretty_print;
33
34use alloy_primitives::Address;
35pub use analysis::SnapshotAnalysis;
36
37use std::{
38    ops::{Deref, DerefMut},
39    sync::Arc,
40};
41
42use edb_common::types::ExecutionFrameId;
43use revm::{database::CacheDB, Database, DatabaseCommit, DatabaseRef};
44use serde::{Deserialize, Serialize};
45use tracing::error;
46
47use crate::{HookSnapshot, HookSnapshots, OpcodeSnapshot, OpcodeSnapshots, USID};
48
49/// Union type representing either an opcode or hook snapshot
50///
51/// This enum allows us to treat both types of snapshots uniformly while preserving
52/// their specific characteristics. Hook snapshots are generally preferred as they
53/// represent strategic breakpoints, while opcode snapshots provide fine-grained
54/// instruction-level details.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Snapshot<DB>
57where
58    DB: Database + DatabaseCommit + DatabaseRef + Clone,
59    <CacheDB<DB> as Database>::Error: Clone,
60    <DB as Database>::Error: Clone,
61{
62    id: usize,
63    frame_id: ExecutionFrameId,
64    next_id: Option<usize>,
65    prev_id: Option<usize>,
66
67    /// Detail of the snapshot
68    detail: SnapshotDetail<DB>,
69}
70
71/// Union type representing details of either an opcode or hook snapshot
72///
73/// This enum allows us to treat both types of snapshots uniformly while preserving
74/// their specific characteristics. Hook snapshots are generally preferred as they
75/// represent strategic breakpoints, while opcode snapshots provide fine-grained
76/// instruction-level details.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub enum SnapshotDetail<DB>
79where
80    DB: Database + DatabaseCommit + DatabaseRef + Clone,
81    <CacheDB<DB> as Database>::Error: Clone,
82    <DB as Database>::Error: Clone,
83{
84    /// Fine-grained opcode execution snapshot
85    Opcode(OpcodeSnapshot<DB>),
86    /// Strategic hook-based snapshot at instrumentation points
87    Hook(HookSnapshot<DB>),
88}
89
90impl<DB> Snapshot<DB>
91where
92    DB: Database + DatabaseCommit + DatabaseRef + Clone,
93    <CacheDB<DB> as Database>::Error: Clone,
94    <DB as Database>::Error: Clone,
95{
96    /// Create an opcode snapshot
97    pub fn new_opcode(id: usize, frame_id: ExecutionFrameId, detail: OpcodeSnapshot<DB>) -> Self {
98        Self { id, frame_id, next_id: None, prev_id: None, detail: SnapshotDetail::Opcode(detail) }
99    }
100
101    /// Create a hook snapshot
102    pub fn new_hook(id: usize, frame_id: ExecutionFrameId, detail: HookSnapshot<DB>) -> Self {
103        Self { id, frame_id, next_id: None, prev_id: None, detail: SnapshotDetail::Hook(detail) }
104    }
105
106    /// Set the id of the next snapshot
107    pub fn set_next_id(&mut self, id: usize) {
108        self.next_id = Some(id);
109    }
110
111    /// Get the id of the next snapshot
112    pub fn next_id(&self) -> Option<usize> {
113        self.next_id
114    }
115
116    /// Set the id of the previous snapshot
117    pub fn set_prev_id(&mut self, id: usize) {
118        self.prev_id = Some(id);
119    }
120
121    /// Get the id of the previous snapshot
122    pub fn prev_id(&self) -> Option<usize> {
123        self.prev_id
124    }
125
126    /// Get the snapshot id
127    pub fn id(&self) -> usize {
128        self.id
129    }
130
131    /// Get the execution frame id
132    pub fn frame_id(&self) -> ExecutionFrameId {
133        self.frame_id
134    }
135
136    /// Get the detail of the snapshot
137    pub fn detail(&self) -> &SnapshotDetail<DB> {
138        &self.detail
139    }
140
141    /// Get the detail of the snapshot (mutable)
142    pub fn detail_mut(&mut self) -> &mut SnapshotDetail<DB> {
143        &mut self.detail
144    }
145
146    /// Get USID if the snapshot is hooked
147    pub fn usid(&self) -> Option<USID> {
148        match &self.detail {
149            SnapshotDetail::Opcode(_) => None,
150            SnapshotDetail::Hook(snapshot) => Some(snapshot.usid),
151        }
152    }
153
154    /// Get DB
155    pub fn db(&self) -> Arc<CacheDB<DB>> {
156        match &self.detail {
157            SnapshotDetail::Opcode(snapshot) => snapshot.database.clone(),
158            SnapshotDetail::Hook(snapshot) => snapshot.database.clone(),
159        }
160    }
161
162    /// Get the contract address associated with this snapshot
163    pub fn bytecode_address(&self) -> Address {
164        match &self.detail {
165            SnapshotDetail::Opcode(snapshot) => snapshot.bytecode_address,
166            SnapshotDetail::Hook(snapshot) => snapshot.bytecode_address,
167        }
168    }
169
170    /// Get the target address associated with this snapshot
171    pub fn target_address(&self) -> Address {
172        match &self.detail {
173            SnapshotDetail::Opcode(snapshot) => snapshot.target_address,
174            SnapshotDetail::Hook(snapshot) => snapshot.target_address,
175        }
176    }
177
178    /// Check if this is a hook snapshot
179    pub fn is_hook(&self) -> bool {
180        matches!(self.detail, SnapshotDetail::Hook(_))
181    }
182
183    /// Check if this is an opcode snapshot  
184    pub fn is_opcode(&self) -> bool {
185        matches!(self.detail, SnapshotDetail::Opcode(_))
186    }
187}
188
189/// Unified collection of execution snapshots organized by execution frame
190///
191/// This structure maintains a chronological view of all captured snapshots,
192/// whether they are fine-grained opcode snapshots or strategic hook snapshots.
193/// Multiple snapshots can exist within a single execution frame, representing
194/// different points of interest during that frame's execution.
195///
196/// The collection prioritizes hook snapshots when available, as they represent
197/// intentional debugging breakpoints. Opcode snapshots are included for frames
198/// that lack hook coverage, ensuring comprehensive state tracking.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Snapshots<DB>
201where
202    DB: Database + DatabaseCommit + DatabaseRef + Clone,
203    <CacheDB<DB> as Database>::Error: Clone,
204    <DB as Database>::Error: Clone,
205{
206    /// Vector of (frame_id, snapshot) pairs in execution order
207    inner: Vec<(ExecutionFrameId, Snapshot<DB>)>,
208}
209
210impl<DB> Deref for Snapshots<DB>
211where
212    DB: Database + DatabaseCommit + DatabaseRef + Clone,
213    <CacheDB<DB> as Database>::Error: Clone,
214    <DB as Database>::Error: Clone,
215{
216    type Target = [(ExecutionFrameId, Snapshot<DB>)];
217
218    fn deref(&self) -> &Self::Target {
219        &self.inner
220    }
221}
222
223impl<DB> DerefMut for Snapshots<DB>
224where
225    DB: Database + DatabaseCommit + DatabaseRef + Clone,
226    <CacheDB<DB> as Database>::Error: Clone,
227    <DB as Database>::Error: Clone,
228{
229    fn deref_mut(&mut self) -> &mut Self::Target {
230        &mut self.inner
231    }
232}
233
234// IntoIterator for owned Trace (moves out its contents)
235impl<DB> IntoIterator for Snapshots<DB>
236where
237    DB: Database + DatabaseCommit + DatabaseRef + Clone,
238    <CacheDB<DB> as Database>::Error: Clone,
239    <DB as Database>::Error: Clone,
240{
241    type Item = (ExecutionFrameId, Snapshot<DB>);
242    type IntoIter = std::vec::IntoIter<(ExecutionFrameId, Snapshot<DB>)>;
243    fn into_iter(self) -> Self::IntoIter {
244        self.inner.into_iter()
245    }
246}
247
248// IntoIterator for &Trace (shared iteration)
249impl<'a, DB> IntoIterator for &'a Snapshots<DB>
250where
251    DB: Database + DatabaseCommit + DatabaseRef + Clone,
252    <CacheDB<DB> as Database>::Error: Clone,
253    <DB as Database>::Error: Clone,
254{
255    type Item = &'a (ExecutionFrameId, Snapshot<DB>);
256    type IntoIter = std::slice::Iter<'a, (ExecutionFrameId, Snapshot<DB>)>;
257    fn into_iter(self) -> Self::IntoIter {
258        self.inner.iter()
259    }
260}
261
262// IntoIterator for &mut Trace (mutable iteration)
263impl<'a, DB> IntoIterator for &'a mut Snapshots<DB>
264where
265    DB: Database + DatabaseCommit + DatabaseRef + Clone,
266    <CacheDB<DB> as Database>::Error: Clone,
267    <DB as Database>::Error: Clone,
268{
269    type Item = &'a mut (ExecutionFrameId, Snapshot<DB>);
270    type IntoIter = std::slice::IterMut<'a, (ExecutionFrameId, Snapshot<DB>)>;
271    fn into_iter(self) -> Self::IntoIter {
272        self.inner.iter_mut()
273    }
274}
275
276impl<DB> Default for Snapshots<DB>
277where
278    DB: Database + DatabaseCommit + DatabaseRef + Clone,
279    <CacheDB<DB> as Database>::Error: Clone,
280    <DB as Database>::Error: Clone,
281{
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287impl<DB> Snapshots<DB>
288where
289    DB: Database + DatabaseCommit + DatabaseRef + Clone,
290    <CacheDB<DB> as Database>::Error: Clone,
291    <DB as Database>::Error: Clone,
292{
293    /// Create a new empty snapshots collection
294    pub fn new() -> Self {
295        Self { inner: Vec::new() }
296    }
297
298    /// Merge hook snapshots and opcode snapshots into a unified collection
299    ///
300    /// This method combines the two snapshot types using the following strategy:
301    /// 1. For frames with hook snapshots, use the hook snapshot (strategic breakpoints)
302    /// 2. For frames without hook snapshots, include all opcode snapshots (fine-grained detail)
303    /// 3. Maintain execution order for proper debugging flow
304    ///
305    /// Hook snapshots are preferred because they represent intentional instrumentation
306    /// points, while opcode snapshots provide comprehensive coverage for uninstrumented code.
307    pub fn merge(
308        mut opcode_snapshots: OpcodeSnapshots<DB>,
309        hook_snapshots: HookSnapshots<DB>,
310    ) -> Self {
311        let mut inner = Vec::new();
312
313        // Process hook snapshots first (they take priority)
314        for (frame_id, snapshot_opt) in hook_snapshots {
315            match snapshot_opt {
316                Some(snapshot) => {
317                    // We have a valid hook snapshot - this takes priority
318                    inner.push((frame_id, Snapshot::new_hook(inner.len(), frame_id, snapshot)));
319                }
320                None => {
321                    // No hook snapshot for this frame - try to use opcode snapshots
322                    // It is possible to be None when a hooked step contains multiple external calls
323                    if let Some(opcode_frame_snapshots) = opcode_snapshots.remove(&frame_id) {
324                        // Add all opcode snapshots for this frame
325                        for opcode_snapshot in opcode_frame_snapshots {
326                            inner.push((
327                                frame_id,
328                                Snapshot::new_opcode(inner.len(), frame_id, opcode_snapshot),
329                            ));
330                        }
331                    }
332                }
333            }
334        }
335
336        // Include any remaining opcode snapshots for frames not covered by hooks
337        if opcode_snapshots.values().any(|snapshots| !snapshots.is_empty()) {
338            error!(
339                "There are still opcode snapshots left after merging: {:?}",
340                opcode_snapshots.keys().collect::<Vec<_>>()
341            );
342        }
343
344        Self { inner }
345    }
346
347    /// Get all snapshots for a specific execution frame
348    pub fn get_frame_snapshots(&self, frame_id: ExecutionFrameId) -> Vec<&Snapshot<DB>> {
349        self.inner
350            .iter()
351            .filter_map(|(id, snapshot)| if *id == frame_id { Some(snapshot) } else { None })
352            .collect()
353    }
354
355    /// Get all unique execution frame IDs that have snapshots
356    pub fn get_frame_ids(&self) -> Vec<ExecutionFrameId> {
357        let mut frame_ids: Vec<_> = self.inner.iter().map(|(id, _)| *id).collect();
358        frame_ids.dedup();
359        frame_ids
360    }
361
362    /// Get the total number of snapshots across all frames
363    pub fn total_snapshot_count(&self) -> usize {
364        self.inner.len()
365    }
366
367    /// Get the number of unique frames that have snapshots
368    pub fn frame_count(&self) -> usize {
369        self.get_frame_ids().len()
370    }
371}