Skip to main content

miden_debug_engine/exec/
snapshot.rs

1//! A self-contained snapshot of a recorded execution, sufficient to replay it in the debugger
2//! without the original host.
3//!
4//! A live execution (e.g. a transaction driven through the DAP executor) resolves two kinds of
5//! host interaction that a bare debugger host cannot reproduce on its own: the advice mutations
6//! returned by event handlers, and the MAST forests resolved for `call`/`dyncall` targets (account
7//! code, note scripts, etc.). A [ReplaySnapshot] captures both, alongside the program and its
8//! inputs, so the same execution can be re-run later by feeding the recorded event log into an
9//! event-replay debugger host (see [`Executor::into_debug_with_replay`](crate::exec::Executor) and
10//! `State::new_for_transaction`).
11
12use std::{
13    path::{Path, PathBuf},
14    sync::{Arc, Mutex},
15};
16
17use miden_core::{
18    mast::MastForest,
19    program::StackInputs,
20    serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
21};
22use miden_mast_package::{Package, debug_info::PackageDebugInfo};
23use miden_processor::{
24    ExecutionOptions, LoadedMastForest,
25    advice::{AdviceInputs, AdviceMutation},
26};
27
28use super::advice::{read_event_log, write_event_log};
29
30/// A shared, cloneable log of the MAST forests a host resolved during execution.
31///
32/// The debugger's event-replay host serves recorded advice mutations for `on_event`, but it still
33/// has to resolve the code for `call`/`dyncall` targets itself. Recording the forests the live
34/// host returned — deduplicated, since the same forest is resolved for many nodes — lets the
35/// replay host load exactly that set and reach the same targets.
36#[derive(Clone, Default)]
37pub struct MastForestRecorder {
38    forests: Arc<Mutex<Vec<LoadedMastForest>>>,
39}
40
41impl MastForestRecorder {
42    /// Create a new, empty recorder.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Returns a copy of the recorded forests.
48    pub fn snapshot(&self) -> Vec<LoadedMastForest> {
49        self.forests.lock().expect("mast forest log poisoned").clone()
50    }
51
52    /// Record a forest resolved by the host, ignoring forests already recorded this run.
53    pub(crate) fn record(&self, forest: LoadedMastForest) {
54        let mut guard = self.forests.lock().expect("mast forest log poisoned");
55        if !guard
56            .iter()
57            .any(|existing| Arc::ptr_eq(existing.mast_forest(), forest.mast_forest()))
58        {
59            guard.push(forest);
60        }
61    }
62
63    /// Discard everything recorded so far, e.g. when execution restarts from the beginning.
64    pub(crate) fn clear(&self) {
65        self.forests.lock().expect("mast forest log poisoned").clear();
66    }
67}
68
69/// Successful replay snapshot write metadata.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct ReplaySnapshotWrite {
72    pub path: PathBuf,
73    pub event_count: usize,
74    pub forest_count: usize,
75}
76
77/// Error metadata for a failed replay snapshot write.
78#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
79#[error("failed to write replay snapshot to {}: {}", path.display(), message)]
80pub struct ReplaySnapshotWriteError {
81    pub path: PathBuf,
82    pub message: String,
83}
84
85/// Shared status handle for a configured replay snapshot write.
86#[derive(Clone, Debug, Default)]
87pub struct ReplaySnapshotRecorder {
88    status: Arc<Mutex<Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>>>>,
89}
90
91impl ReplaySnapshotRecorder {
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Returns the last snapshot write status, leaving the handle empty.
97    pub fn take(&self) -> Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>> {
98        self.status.lock().expect("replay snapshot status poisoned").take()
99    }
100
101    pub(crate) fn record_success(&self, write: ReplaySnapshotWrite) {
102        *self.status.lock().expect("replay snapshot status poisoned") = Some(Ok(write));
103    }
104
105    pub(crate) fn record_error(&self, path: PathBuf, err: impl ToString) {
106        *self.status.lock().expect("replay snapshot status poisoned") =
107            Some(Err(ReplaySnapshotWriteError {
108                path,
109                message: err.to_string(),
110            }));
111    }
112}
113
114/// Magic bytes identifying a replay snapshot file, followed by a format version. Bumping the
115/// version invalidates older snapshots, whose serialized shape may differ.
116const SNAPSHOT_MAGIC: [u8; 6] = *b"MDNSNP";
117const SNAPSHOT_VERSION: u8 = 3;
118
119/// Everything needed to replay a recorded execution in the debugger.
120pub struct ReplaySnapshot {
121    /// The program that was executed (for a transaction, the transaction kernel).
122    pub package: Arc<Package>,
123    /// The operand stack inputs the program started with.
124    pub stack_inputs: StackInputs,
125    /// The advice inputs the program started with.
126    pub advice_inputs: AdviceInputs,
127    /// The VM execution options used by the recorded run.
128    pub options: ExecutionOptions,
129    /// The MAST forests resolved by the host during execution (account code, note scripts, ...),
130    /// which the replay host must be able to resolve for the same `call`/`dyncall` targets.
131    pub mast_forests: Vec<LoadedMastForest>,
132    /// The advice mutations produced by event handlers, one entry per `on_event` invocation, in
133    /// execution order — the event replay queue.
134    pub event_log: Vec<Vec<AdviceMutation>>,
135}
136
137impl ReplaySnapshot {
138    /// Serialize the snapshot to `path`.
139    pub fn write_to_file(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
140        std::fs::write(path, self.to_bytes())
141    }
142
143    /// Read and deserialize a snapshot from `path`.
144    pub fn read_from_file(path: impl AsRef<Path>) -> Result<Self, ReplaySnapshotError> {
145        let bytes = std::fs::read(path).map_err(ReplaySnapshotError::Io)?;
146        Self::read_from_bytes(&bytes).map_err(ReplaySnapshotError::Deserialization)
147    }
148
149    /// Serialize the snapshot to a byte vector.
150    pub fn to_bytes(&self) -> Vec<u8> {
151        let mut bytes = Vec::new();
152        self.write_into(&mut bytes);
153        bytes
154    }
155
156    /// Deserialize a snapshot from bytes.
157    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
158        let mut reader = miden_core::serde::SliceReader::new(bytes);
159        Self::read_from(&mut reader)
160    }
161}
162
163impl Serializable for ReplaySnapshot {
164    fn write_into<W: ByteWriter>(&self, target: &mut W) {
165        target.write_bytes(&SNAPSHOT_MAGIC);
166        target.write_u8(SNAPSHOT_VERSION);
167        self.package.write_into(target);
168        self.stack_inputs.write_into(target);
169        self.advice_inputs.write_into(target);
170        write_execution_options(&self.options, target);
171        target.write_usize(self.mast_forests.len());
172        for forest in &self.mast_forests {
173            forest.mast_forest().as_ref().write_into(target);
174            match forest.package_debug_info().ok().flatten() {
175                Some(debug_info) => {
176                    target.write_bool(true);
177                    debug_info.as_ref().write_into(target);
178                }
179                None => {
180                    target.write_bool(false);
181                }
182            }
183        }
184        write_event_log(&self.event_log, target);
185    }
186}
187
188impl Deserializable for ReplaySnapshot {
189    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
190        let magic: [u8; 6] = source.read_array()?;
191        if magic != SNAPSHOT_MAGIC {
192            return Err(DeserializationError::InvalidValue(
193                "not a Miden debugger replay snapshot (bad magic)".to_string(),
194            ));
195        }
196        let version = source.read_u8()?;
197        if version != SNAPSHOT_VERSION {
198            return Err(DeserializationError::InvalidValue(format!(
199                "unsupported replay snapshot version {version} (expected {SNAPSHOT_VERSION})"
200            )));
201        }
202        let package = Arc::new(Package::read_from_trusted(source)?);
203        let stack_inputs = StackInputs::read_from(source)?;
204        let advice_inputs = AdviceInputs::read_from(source)?;
205        let options = read_execution_options(source)?;
206        let forest_count = source.read_usize()?;
207        let mut mast_forests = Vec::with_capacity(forest_count);
208        for _ in 0..forest_count {
209            let mast_forest = Arc::new(MastForest::read_from(source)?);
210            mast_forests.push(if source.read_bool()? {
211                let debug_info = Some(PackageDebugInfo::read_from(source)?);
212                LoadedMastForest::with_package_debug_info(mast_forest, Ok(debug_info))
213            } else {
214                LoadedMastForest::new(mast_forest)
215            });
216        }
217        let event_log = read_event_log(source)?;
218        Ok(Self {
219            package,
220            stack_inputs,
221            advice_inputs,
222            options,
223            mast_forests,
224            event_log,
225        })
226    }
227}
228
229fn write_execution_options<W: ByteWriter>(options: &ExecutionOptions, target: &mut W) {
230    target.write_u32(options.max_cycles());
231    target.write_u32(options.expected_cycles());
232    target.write_usize(options.core_trace_fragment_size());
233    target.write_usize(options.max_advice_size_bytes());
234    target.write_usize(options.max_hash_len_bytes());
235    target.write_bool(options.overlapped_trace_build());
236    target.write_usize(options.max_num_continuations());
237    target.write_usize(options.max_stack_depth());
238    target.write_usize(options.max_memory_elements());
239}
240
241fn read_execution_options<R: ByteReader>(
242    source: &mut R,
243) -> Result<ExecutionOptions, DeserializationError> {
244    let max_cycles = source.read_u32()?;
245    let expected_cycles = source.read_u32()?;
246    let core_trace_fragment_size = source.read_usize()?;
247    let max_advice_size_bytes = source.read_usize()?;
248    let max_hash_len_bytes = source.read_usize()?;
249    let overlapped_trace_build = source.read_bool()?;
250    let max_num_continuations = source.read_usize()?;
251    let max_stack_depth = source.read_usize()?;
252    let max_memory_elements = source.read_usize()?;
253
254    ExecutionOptions::new(Some(max_cycles), expected_cycles, core_trace_fragment_size)
255        .map_err(|err| {
256            DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
257        })
258        .and_then(|options| {
259            options
260                .with_max_advice_size_bytes(max_advice_size_bytes)
261                .with_max_hash_len_bytes(max_hash_len_bytes)
262                .with_overlapped_trace_build(overlapped_trace_build)
263                .with_max_num_continuations(max_num_continuations)
264                .with_max_memory_elements(max_memory_elements)
265                .with_max_stack_depth(max_stack_depth)
266                .map_err(|err| {
267                    DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
268                })
269        })
270}
271
272/// Error reading a [ReplaySnapshot] from a file.
273#[derive(Debug, thiserror::Error)]
274pub enum ReplaySnapshotError {
275    #[error("failed to read replay snapshot file: {0}")]
276    Io(std::io::Error),
277    #[error("failed to deserialize replay snapshot: {0}")]
278    Deserialization(DeserializationError),
279}
280
281#[cfg(test)]
282mod tests {
283    use miden_assembly::{Assembler, DefaultSourceManager};
284    use miden_core::{Felt, Word, crypto::merkle::InnerNodeInfo};
285
286    use super::*;
287
288    fn word(values: [u32; 4]) -> Word {
289        Word::from(values.map(Felt::from))
290    }
291
292    /// A snapshot round-trips through bytes: program, inputs, forests, and the event log — across
293    /// the AdviceMutation variant shapes that carry simple payloads — survive intact.
294    #[test]
295    fn replay_snapshot_round_trips() {
296        let source_manager = Arc::new(DefaultSourceManager::default());
297        let program = Assembler::new(source_manager)
298            .assemble_program("program", "begin push.1 push.2 add drop end")
299            .map(Arc::<Package>::from)
300            .expect("failed to assemble test program");
301        let forest = LoadedMastForest::with_package_debug_info(
302            program.mast_forest().clone(),
303            program.debug_info(),
304        );
305
306        let event_log = vec![
307            vec![AdviceMutation::extend_advice_stack(
308                [Felt::from(7u32), Felt::from(8u32)].into_iter().collect(),
309            )],
310            vec![],
311            vec![AdviceMutation::extend_merkle_store([InnerNodeInfo {
312                value: word([1, 2, 3, 4]),
313                left: word([5, 6, 7, 8]),
314                right: word([9, 10, 11, 12]),
315            }])],
316        ];
317
318        let snapshot = ReplaySnapshot {
319            package: program.clone(),
320            stack_inputs: StackInputs::new(&[Felt::from(42u32), Felt::from(43u32)]).unwrap(),
321            advice_inputs: AdviceInputs::default()
322                .with_stack([Felt::from(99u32)].into_iter().collect()),
323            options: ExecutionOptions::new(Some(100_000), 32, 1024)
324                .unwrap()
325                .with_max_advice_size_bytes(256)
326                .with_max_hash_len_bytes(512)
327                .with_overlapped_trace_build(false)
328                .with_max_num_continuations(128)
329                .with_max_memory_elements(1024)
330                .with_max_stack_depth(128)
331                .unwrap(),
332            mast_forests: vec![forest],
333            event_log,
334        };
335
336        let restored = ReplaySnapshot::read_from_bytes(&snapshot.to_bytes())
337            .expect("snapshot failed to deserialize");
338
339        assert_eq!(restored.package.commitment(), snapshot.package.commitment());
340        assert_eq!(restored.stack_inputs, snapshot.stack_inputs);
341        assert_eq!(restored.advice_inputs, snapshot.advice_inputs);
342        assert_eq!(restored.options, snapshot.options);
343        assert_eq!(restored.mast_forests.len(), 1);
344        assert_eq!(restored.event_log.len(), 3);
345        assert_eq!(restored.event_log[1].len(), 0, "empty event batch must survive");
346        match restored.event_log[0].as_slice() {
347            [AdviceMutation::ExtendStack { stack }] => {
348                assert_eq!(
349                    stack.iter().copied().collect::<Vec<_>>(),
350                    [Felt::from(7u32), Felt::from(8u32)],
351                );
352            }
353            _ => panic!("unexpected first event batch"),
354        }
355        match restored.event_log[2].as_slice() {
356            [AdviceMutation::ExtendMerkleStore { inner_nodes }] => {
357                assert_eq!(inner_nodes.len(), 1);
358                assert_eq!(inner_nodes[0].value, word([1, 2, 3, 4]));
359                assert_eq!(inner_nodes[0].right, word([9, 10, 11, 12]));
360            }
361            _ => panic!("unexpected merkle-store event batch"),
362        }
363    }
364
365    /// A file that does not start with the snapshot magic is rejected.
366    #[test]
367    fn replay_snapshot_rejects_bad_magic() {
368        let err = ReplaySnapshot::read_from_bytes(b"not a snapshot at all really");
369        assert!(err.is_err(), "expected deserialization to fail on bad magic");
370    }
371
372    /// Version 1 snapshots may contain legacy precompile mutations and opcode semantics, so they
373    /// must not be interpreted as version 2 snapshots.
374    #[test]
375    fn replay_snapshot_rejects_previous_version() {
376        let mut bytes = SNAPSHOT_MAGIC.to_vec();
377        bytes.push(SNAPSHOT_VERSION - 1);
378
379        let Err(err) = ReplaySnapshot::read_from_bytes(&bytes) else {
380            panic!("expected deserialization to reject a snapshot from a previous version");
381        };
382        assert!(err.to_string().contains("unsupported replay snapshot version"));
383    }
384}