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