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 = 1;
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_unchecked(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_adv_map_value_size());
234    target.write_usize(options.max_adv_map_elements());
235    target.write_usize(options.max_hash_len_bytes());
236    target.write_usize(options.max_num_continuations());
237    target.write_usize(options.max_stack_depth());
238}
239
240fn read_execution_options<R: ByteReader>(
241    source: &mut R,
242) -> Result<ExecutionOptions, DeserializationError> {
243    let max_cycles = source.read_u32()?;
244    let expected_cycles = source.read_u32()?;
245    let core_trace_fragment_size = source.read_usize()?;
246    let max_adv_map_value_size = source.read_usize()?;
247    let max_adv_map_elements = source.read_usize()?;
248    let max_hash_len_bytes = source.read_usize()?;
249    let max_num_continuations = source.read_usize()?;
250    let max_stack_depth = source.read_usize()?;
251
252    ExecutionOptions::new(Some(max_cycles), expected_cycles, core_trace_fragment_size)
253        .map_err(|err| {
254            DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
255        })
256        .and_then(|options| {
257            options
258                .with_max_adv_map_value_size(max_adv_map_value_size)
259                .with_max_adv_map_elements(max_adv_map_elements)
260                .with_max_hash_len_bytes(max_hash_len_bytes)
261                .with_max_num_continuations(max_num_continuations)
262                .with_max_stack_depth(max_stack_depth)
263                .map_err(|err| {
264                    DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
265                })
266        })
267}
268
269/// Error reading a [ReplaySnapshot] from a file.
270#[derive(Debug, thiserror::Error)]
271pub enum ReplaySnapshotError {
272    #[error("failed to read replay snapshot file: {0}")]
273    Io(std::io::Error),
274    #[error("failed to deserialize replay snapshot: {0}")]
275    Deserialization(DeserializationError),
276}
277
278#[cfg(test)]
279mod tests {
280    use miden_assembly::{Assembler, DefaultSourceManager};
281    use miden_core::{Felt, Word, crypto::merkle::InnerNodeInfo};
282
283    use super::*;
284
285    fn word(values: [u32; 4]) -> Word {
286        Word::from(values.map(Felt::from))
287    }
288
289    /// A snapshot round-trips through bytes: program, inputs, forests, and the event log — across
290    /// the AdviceMutation variant shapes that carry simple payloads — survive intact.
291    #[test]
292    fn replay_snapshot_round_trips() {
293        let source_manager = Arc::new(DefaultSourceManager::default());
294        let program = Assembler::new(source_manager)
295            .assemble_program("program", "begin push.1 push.2 add drop end")
296            .map(Arc::<Package>::from)
297            .expect("failed to assemble test program");
298        let forest = LoadedMastForest::with_package_debug_info(
299            program.mast_forest().clone(),
300            program.debug_info(),
301        );
302
303        let event_log = vec![
304            vec![AdviceMutation::extend_stack([Felt::from(7u32), Felt::from(8u32)])],
305            vec![],
306            vec![AdviceMutation::extend_merkle_store([InnerNodeInfo {
307                value: word([1, 2, 3, 4]),
308                left: word([5, 6, 7, 8]),
309                right: word([9, 10, 11, 12]),
310            }])],
311        ];
312
313        let snapshot = ReplaySnapshot {
314            package: program.clone(),
315            stack_inputs: StackInputs::new(&[Felt::from(42u32), Felt::from(43u32)]).unwrap(),
316            advice_inputs: AdviceInputs::default().with_stack([Felt::from(99u32)]),
317            options: ExecutionOptions::new(Some(100_000), 32, 1024)
318                .unwrap()
319                .with_max_adv_map_value_size(64)
320                .with_max_adv_map_elements(256)
321                .with_max_hash_len_bytes(512)
322                .with_max_num_continuations(128)
323                .with_max_stack_depth(128)
324                .unwrap(),
325            mast_forests: vec![forest],
326            event_log,
327        };
328
329        let restored = ReplaySnapshot::read_from_bytes(&snapshot.to_bytes())
330            .expect("snapshot failed to deserialize");
331
332        assert_eq!(restored.package.digest(), snapshot.package.digest());
333        assert_eq!(restored.stack_inputs, snapshot.stack_inputs);
334        assert_eq!(restored.advice_inputs, snapshot.advice_inputs);
335        assert_eq!(restored.options, snapshot.options);
336        assert_eq!(restored.mast_forests.len(), 1);
337        assert_eq!(restored.event_log.len(), 3);
338        assert_eq!(restored.event_log[1].len(), 0, "empty event batch must survive");
339        match restored.event_log[0].as_slice() {
340            [AdviceMutation::ExtendStack { values }] => {
341                assert_eq!(values.as_slice(), &[Felt::from(7u32), Felt::from(8u32)]);
342            }
343            _ => panic!("unexpected first event batch"),
344        }
345        match restored.event_log[2].as_slice() {
346            [AdviceMutation::ExtendMerkleStore { infos }] => {
347                assert_eq!(infos.len(), 1);
348                assert_eq!(infos[0].value, word([1, 2, 3, 4]));
349                assert_eq!(infos[0].right, word([9, 10, 11, 12]));
350            }
351            _ => panic!("unexpected merkle-store event batch"),
352        }
353    }
354
355    /// A file that does not start with the snapshot magic is rejected.
356    #[test]
357    fn replay_snapshot_rejects_bad_magic() {
358        let err = ReplaySnapshot::read_from_bytes(b"not a snapshot at all really");
359        assert!(err.is_err(), "expected deserialization to fail on bad magic");
360    }
361}