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    pub(crate) fn record(&self, forest: LoadedMastForest) {
57        let mut guard = self.forests.write();
58        if !guard
59            .iter()
60            .any(|existing| Arc::ptr_eq(existing.mast_forest(), forest.mast_forest()))
61        {
62            guard.push(forest);
63        }
64    }
65
66    /// Discard everything recorded so far, e.g. when execution restarts from the beginning.
67    #[cfg(feature = "dap")]
68    pub(crate) fn clear(&self) {
69        self.forests.write().clear();
70    }
71}
72
73/// Successful replay snapshot write metadata.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ReplaySnapshotWrite {
76    pub path: Uri,
77    pub event_count: usize,
78    pub forest_count: usize,
79}
80
81/// Error metadata for a failed replay snapshot write.
82#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
83#[error("failed to write replay snapshot to {}: {}", path, message)]
84pub struct ReplaySnapshotWriteError {
85    pub path: Uri,
86    pub message: String,
87}
88
89/// Shared status handle for a configured replay snapshot write.
90#[derive(Clone, Debug, Default)]
91pub struct ReplaySnapshotRecorder {
92    status: Arc<RwLock<Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>>>>,
93}
94
95impl ReplaySnapshotRecorder {
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Returns the last snapshot write status, leaving the handle empty.
101    pub fn take(&self) -> Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>> {
102        self.status.write().take()
103    }
104
105    #[cfg(feature = "dap")]
106    pub(crate) fn record_success(&self, write: ReplaySnapshotWrite) {
107        *self.status.write() = Some(Ok(write));
108    }
109
110    #[cfg(feature = "dap")]
111    pub(crate) fn record_error(&self, path: Uri, err: impl ToString) {
112        *self.status.write() = Some(Err(ReplaySnapshotWriteError {
113            path,
114            message: err.to_string(),
115        }));
116    }
117}
118
119/// Magic bytes identifying a replay snapshot file, followed by a format version. Bumping the
120/// version invalidates older snapshots, whose serialized shape may differ.
121const SNAPSHOT_MAGIC: [u8; 6] = *b"MDNSNP";
122const SNAPSHOT_VERSION: u8 = 3;
123
124/// Everything needed to replay a recorded execution in the debugger.
125pub struct ReplaySnapshot {
126    /// The program that was executed (for a transaction, the transaction kernel).
127    pub package: Arc<Package>,
128    /// The operand stack inputs the program started with.
129    pub stack_inputs: StackInputs,
130    /// The advice inputs the program started with.
131    pub advice_inputs: AdviceInputs,
132    /// The VM execution options used by the recorded run.
133    pub options: ExecutionOptions,
134    /// The MAST forests resolved by the host during execution (account code, note scripts, ...),
135    /// which the replay host must be able to resolve for the same `call`/`dyncall` targets.
136    pub mast_forests: Vec<LoadedMastForest>,
137    /// The advice mutations produced by event handlers, one entry per `on_event` invocation, in
138    /// execution order — the event replay queue.
139    pub event_log: Vec<Vec<AdviceMutation>>,
140}
141
142impl ReplaySnapshot {
143    /// Serialize the snapshot to `path`.
144    #[cfg(feature = "std")]
145    pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
146        std::fs::write(path, self.to_bytes())
147    }
148
149    /// Read and deserialize a snapshot from `path`.
150    #[cfg(feature = "std")]
151    pub fn read_from_file(path: impl AsRef<std::path::Path>) -> Result<Self, ReplaySnapshotError> {
152        let bytes = std::fs::read(path).map_err(ReplaySnapshotError::Io)?;
153        Self::read_from_bytes(&bytes).map_err(ReplaySnapshotError::Deserialization)
154    }
155
156    /// Serialize the snapshot to a byte vector.
157    pub fn to_bytes(&self) -> Vec<u8> {
158        let mut bytes = Vec::new();
159        self.write_into(&mut bytes);
160        bytes
161    }
162
163    /// Deserialize a snapshot from bytes.
164    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
165        let mut reader = miden_core::serde::SliceReader::new(bytes);
166        Self::read_from(&mut reader)
167    }
168}
169
170impl Serializable for ReplaySnapshot {
171    fn write_into<W: ByteWriter>(&self, target: &mut W) {
172        target.write_bytes(&SNAPSHOT_MAGIC);
173        target.write_u8(SNAPSHOT_VERSION);
174        self.package.write_into(target);
175        self.stack_inputs.write_into(target);
176        self.advice_inputs.write_into(target);
177        write_execution_options(&self.options, target);
178        target.write_usize(self.mast_forests.len());
179        for forest in &self.mast_forests {
180            forest.mast_forest().as_ref().write_into(target);
181            match forest.package_debug_info().ok().flatten() {
182                Some(debug_info) => {
183                    target.write_bool(true);
184                    debug_info.as_ref().write_into(target);
185                }
186                None => {
187                    target.write_bool(false);
188                }
189            }
190        }
191        write_event_log(&self.event_log, target);
192    }
193}
194
195impl Deserializable for ReplaySnapshot {
196    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
197        let magic: [u8; 6] = source.read_array()?;
198        if magic != SNAPSHOT_MAGIC {
199            return Err(DeserializationError::InvalidValue(
200                "not a Miden debugger replay snapshot (bad magic)".to_string(),
201            ));
202        }
203        let version = source.read_u8()?;
204        if version != SNAPSHOT_VERSION {
205            return Err(DeserializationError::InvalidValue(format!(
206                "unsupported replay snapshot version {version} (expected {SNAPSHOT_VERSION})"
207            )));
208        }
209        let package = Arc::new(Package::read_from_trusted(source)?);
210        let stack_inputs = StackInputs::read_from(source)?;
211        let advice_inputs = AdviceInputs::read_from(source)?;
212        let options = read_execution_options(source)?;
213        let forest_count = source.read_usize()?;
214        let mut mast_forests = Vec::with_capacity(forest_count);
215        for _ in 0..forest_count {
216            let mast_forest = Arc::new(MastForest::read_from(source)?);
217            mast_forests.push(if source.read_bool()? {
218                let debug_info = Some(PackageDebugInfo::read_from(source)?);
219                LoadedMastForest::with_package_debug_info(mast_forest, Ok(debug_info))
220            } else {
221                LoadedMastForest::new(mast_forest)
222            });
223        }
224        let event_log = read_event_log(source)?;
225        Ok(Self {
226            package,
227            stack_inputs,
228            advice_inputs,
229            options,
230            mast_forests,
231            event_log,
232        })
233    }
234}
235
236fn write_execution_options<W: ByteWriter>(options: &ExecutionOptions, target: &mut W) {
237    target.write_u32(options.max_cycles());
238    target.write_u32(options.expected_cycles());
239    target.write_usize(options.core_trace_fragment_size());
240    target.write_usize(options.max_advice_size_bytes());
241    target.write_usize(options.max_hash_len_bytes());
242    target.write_bool(options.overlapped_trace_build());
243    target.write_usize(options.max_num_continuations());
244    target.write_usize(options.max_stack_depth());
245    target.write_usize(options.max_memory_elements());
246}
247
248fn read_execution_options<R: ByteReader>(
249    source: &mut R,
250) -> Result<ExecutionOptions, DeserializationError> {
251    let max_cycles = source.read_u32()?;
252    let expected_cycles = source.read_u32()?;
253    let core_trace_fragment_size = source.read_usize()?;
254    let max_advice_size_bytes = source.read_usize()?;
255    let max_hash_len_bytes = source.read_usize()?;
256    let overlapped_trace_build = source.read_bool()?;
257    let max_num_continuations = source.read_usize()?;
258    let max_stack_depth = source.read_usize()?;
259    let max_memory_elements = source.read_usize()?;
260
261    ExecutionOptions::new(Some(max_cycles), expected_cycles, core_trace_fragment_size)
262        .map_err(|err| {
263            DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
264        })
265        .and_then(|options| {
266            options
267                .with_max_advice_size_bytes(max_advice_size_bytes)
268                .with_max_hash_len_bytes(max_hash_len_bytes)
269                .with_overlapped_trace_build(overlapped_trace_build)
270                .with_max_num_continuations(max_num_continuations)
271                .with_max_memory_elements(max_memory_elements)
272                .with_max_stack_depth(max_stack_depth)
273                .map_err(|err| {
274                    DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
275                })
276        })
277}
278
279/// Error reading a [ReplaySnapshot] from a file.
280#[derive(Debug, thiserror::Error)]
281#[cfg(feature = "std")]
282pub enum ReplaySnapshotError {
283    #[error("failed to read replay snapshot file: {0}")]
284    Io(std::io::Error),
285    #[error("failed to deserialize replay snapshot: {0}")]
286    Deserialization(DeserializationError),
287}
288
289#[cfg(test)]
290mod tests;