Skip to main content

miden_core/mast/serialization/
mod.rs

1//! MAST forest serialization keeps one fixed structural layout for normal and hashless payloads.
2//!
3//! The main goal is to keep random access cheap in both modes. Node structure
4//! stays in one fixed-width section. Variable-size data lives in separate sections. Internal node
5//! digests also live in a separate section so hashless payloads can omit them without changing the
6//! structural layout.
7//!
8//! Wire flags describe serializer intent, not reader trust policy. Trusted [`MastForest`] reads
9//! reject hashless payloads. [`crate::mast::UntrustedMastForest`] accepts them and rebuilds
10//! non-external digests before use. If a non-hashless payload is sent down the untrusted path,
11//! validation recomputes those digests and requires them to match the serialized values.
12//! Budgeted untrusted reads always bound wire counts during layout scanning via
13//! [`ByteReader::max_alloc`]. Validation also gets a second check:
14//! - later hashless helper allocations are charged against a validation budget before the
15//!   corresponding `Vec` or CSR scaffolding is created
16//! - that budget is derived from the wire budget by a coarse multiplier; this is intentionally a
17//!   simple bound for common callers, not an exact peak-memory formula
18//!
19//! The main layers fit together like this:
20//!
21//! ```text
22//! wire bytes
23//!     |
24//!     +--> ForestLayout -----------> MastForestWireView ----+
25//!     |        absolute offsets         trusted cache view   |
26//!     |                                                     v
27//!     +--> UntrustedMastForest ----validate----> ResolvedSerializedForest ---> MastForest
28//!              bytes + parsed state                digest-backed view            trusted runtime
29//!
30//! MastForestView is the shared random-access API implemented by MastForestWireView and
31//! MastForest.
32//! ```
33//!
34//! The format is:
35//!
36//! (Metadata)
37//! - MAGIC (4 bytes) + FLAGS (1 byte) + VERSION (3 bytes)
38//!
39//! (Counts)
40//! - internal nodes count (`usize`)
41//! - external nodes count (`usize`)
42//!
43//! (Procedure roots section)
44//! - procedure roots (`Vec<u32>` as MastNodeId values)
45//!
46//! (Basic block data section)
47//! - basic block data (padded operations + batch metadata)
48//!
49//! (Node entries section)
50//! - fixed-width structural node entries (`Vec<MastNodeEntry>`)
51//! - `Block` entries store offsets into the basic-block section above
52//!
53//! (External digest section)
54//! - digests for `External` nodes only (`Vec<Word>`, ordered by node index)
55//! - lookup is dense-by-kind: the Nth external node uses slot N in this section
56//!
57//! (Node hash section - omitted if FLAGS bit 1 is set)
58//! - digests for all non-external nodes (`Vec<Word>`, ordered by node index)
59//! - lookup is also dense-by-kind: the Nth non-external node uses slot N in this section
60//!
61//! (Advice map section)
62//! - Advice map (`AdviceMap`)
63//!
64//! (No trailing debug section)
65//!
66//! Readers reject any trailing payload after the advice map. Package-owned debug sections are now
67//! the only supported debug serialization path.
68//!
69//! In hashless format, the internal node-hash section is omitted. External node digests still stay
70//! on the wire because they cannot be rebuilt from local structure. This keeps hashless focused on
71//! the untrusted-validation use case: trusted reads reject `HASHLESS`, and the untrusted path
72//! rebuilds the data it actually trusts before use.
73//!
74//! Readers recover per-node digest lookup by scanning node entries once and building a compact
75//! "slot by node index" table. This preserves random access without forcing all digests into the
76//! same contiguous array on the wire.
77//!
78//! Public entry points adopt these policies:
79//! - [`MastForest::read_from_bytes`]: trusted execution payload, no hashless support.
80//! - [`MastForestWireView::new`]: trusted wire-backed cache access; rejects hashless and legacy
81//!   debug-bearing payloads.
82//! - [`crate::mast::UntrustedMastForest::read_from_bytes`] /
83//!   [`crate::mast::UntrustedMastForest::read_from_bytes_with_options`]: untrusted parsing plus
84//!   later validation before use.
85
86#[cfg(test)]
87use alloc::string::ToString;
88use alloc::{boxed::Box, format, vec::Vec};
89use core::mem::size_of;
90
91use miden_utils_sync::OnceLockCompat;
92
93use super::{MastForest, MastNode, MastNodeId};
94use crate::{
95    Word,
96    advice::AdviceMap,
97    mast::node::MastNodeExt,
98    serde::{
99        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
100        SliceReader,
101    },
102};
103
104mod info;
105pub use info::{MastNodeEntry, MastNodeInfo};
106
107mod view;
108use view::WireAdviceMapView;
109pub use view::{AdviceMapView, AdviceValueView, MastForestView};
110
111mod layout;
112pub(super) use layout::ForestLayout;
113use layout::{OffsetTrackingReader, TrackingReader, WireFlags, read_header_and_scan_layout};
114
115mod resolved;
116use resolved::{ResolvedSerializedForest, basic_block_offset_for_node_index};
117
118mod basic_blocks;
119use basic_blocks::{BasicBlockDataBuilder, basic_block_data_len};
120
121#[cfg(test)]
122mod seed_gen;
123
124#[cfg(test)]
125mod tests;
126
127// TYPE ALIASES
128// ================================================================================================
129
130/// Specifies an offset into the `node_data` section of an encoded [`MastForest`].
131type NodeDataOffset = u32;
132
133/// Default multiplier for the untrusted validation allocation budget.
134///
135/// The budgeted byte reader limits wire-driven parsing. Hashless validation also needs transient
136/// per-node allocations for the slot table and rebuilt digest data.
137/// The generic untrusted path also retains a recorded copy of the consumed
138/// serialized payload for deferred validation.
139///
140/// This convenience multiplier is therefore a coarse "wire bytes plus worst-case helper
141/// headroom" bound:
142/// - `* 6` covers the helper-allocation model introduced with explicit validation budgeting
143/// - `+ 1 * bytes_len` covers the retained serialized copy recorded during untrusted reads
144///
145/// It is deliberately conservative and exists to make the default
146/// [`crate::mast::UntrustedMastForest::read_from_bytes`] path usable without forcing callers to
147/// size each helper allocation themselves. Callers with stricter limits should use
148/// [`crate::mast::UntrustedMastForest::read_from_bytes_with_options`] and choose an explicit wire
149/// budget; the validation helper budget is derived from it.
150const DEFAULT_UNTRUSTED_ALLOCATION_BUDGET_MULTIPLIER: usize = 7;
151
152/// Byte-read budget multiplier for trusted full deserialization from a byte slice.
153///
154/// The budget is intentionally finite to reject malicious length prefixes, but larger than the
155/// source length because collection deserialization uses conservative per-element size estimates.
156const TRUSTED_BYTE_READ_BUDGET_MULTIPLIER: usize = 64;
157
158// CONSTANTS
159// ================================================================================================
160
161/// Magic bytes for detecting that a file is binary-encoded MAST.
162///
163/// The header is `b"MAST"` + flags byte + version bytes.
164///
165/// This repurposes the old `b"MAST\0"` terminator as the flags byte.
166const MAGIC: &[u8; 4] = b"MAST";
167
168/// Flag indicating that the internal node-hash section is omitted from the wire payload.
169///
170/// External digests still remain serialized in their own section because they cannot be rebuilt
171/// from local structure.
172pub(super) const FLAG_HASHLESS: u8 = 0x02;
173
174/// Mask for reserved flag bits that must be zero.
175///
176/// Bit 0 and bits 2-7 are reserved for future use. If any are set, deserialization fails.
177const FLAGS_RESERVED_MASK: u8 = 0xfd;
178
179/// The format version.
180///
181/// If future modifications are made to this format, the version should be incremented by 1. A
182/// version of `[255, 255, 255]` is reserved for future extensions that require extending the
183/// version field itself, but should be considered invalid for now.
184///
185/// Version history:
186/// - [0, 0, 0]: Initial format.
187/// - [0, 0, 1]: Added batch metadata to basic blocks (operations serialized in padded form with
188///   indptr, padding, and group metadata for exact OpBatch reconstruction). Added asm-op metadata
189///   and debug-variable storage in CSR layout (eliminates per-node metadata sections and round-trip
190///   conversions). Header changed from `MAST\0` to `MAST` + flags byte.
191/// - [0, 0, 2]: AssemblyOps moved out of inline metadata into a dedicated DebugInfo section.
192///   Removed `should_break` field from AssemblyOp serialization (#2646). Removed `breakpoint`
193///   instruction (#2655).
194/// - [0, 0, 3]: Added HASHLESS flag (bit 1). Trusted deserialization rejects HASHLESS. Split
195///   fixed-width node entries from digest storage. External digests moved to a dedicated section.
196///   Hashless serialization omits the general node-hash section entirely. Removed the unused
197///   metadata-count field from the wire header. Before any public release on this branch, the same
198///   unreleased wire version also grew explicit internal/external node counts in the header.
199/// - [0, 0, 4]: Removed the legacy inline metadata wire slots entirely. All assembly op metadata
200///   and debug variable metadata are now stored in the DebugInfo section as separate indexed
201///   records. MAST nodes are metadata-free identifiers. Before any public release on this branch,
202///   the same unreleased wire version also reserved bit 0 and stopped using it as a forest-level
203///   debug-presence flag.
204///
205/// Legacy wire versions (pre-#3192 decorator terminology):
206///   [0,0,1] stored metadata as serialized decorator variants in CSR per-node slots.
207///   [0,0,2] removed AssemblyOp from the decorator enum and stored them separately in DebugInfo.
208///   [0,0,3] removed the unused decorator-count wire field.
209///   [0,0,4] eliminated the decorator wire slots entirely.
210const VERSION: [u8; 3] = [0, 0, 4];
211
212// MAST FOREST SERIALIZATION/DESERIALIZATION
213// ================================================================================================
214
215impl Serializable for MastForest {
216    fn write_into<W: ByteWriter>(&self, target: &mut W) {
217        self.write_into_with_options(target, false);
218    }
219}
220
221impl MastForest {
222    /// Internal serialization with options.
223    ///
224    /// Current writers encode normal execution payloads or hashless validation payloads.
225    /// Both forms use the finalized dense node order already stored in the `MastForest`; writers
226    /// validate that order but do not sort nodes while writing.
227    fn write_into_with_options<W: ByteWriter>(&self, target: &mut W, hashless: bool) {
228        self.validate_dense_node_order()
229            .expect("dense MAST forest must be in final dense order before serialization");
230
231        let mut basic_block_data_builder = BasicBlockDataBuilder::new();
232
233        // magic & flags
234        target.write_bytes(MAGIC);
235        let flags = if hashless { FLAG_HASHLESS } else { 0 };
236        target.write_u8(flags);
237
238        // version
239        target.write_bytes(&VERSION);
240
241        // header counts
242        let node_count = self.nodes.len();
243        let external_node_count = self.nodes.iter().take_while(|node| node.is_external()).count();
244        let internal_node_count = node_count - external_node_count;
245        target.write_usize(internal_node_count);
246        target.write_usize(external_node_count);
247
248        // roots
249        let roots: Vec<u32> = self.roots.iter().copied().map(u32::from).collect();
250        roots.write_into(target);
251
252        let mut mast_node_entries = Vec::with_capacity(self.nodes.len());
253        let mut external_digests = Vec::with_capacity(external_node_count);
254        let mut node_hashes = Vec::new();
255
256        for mast_node in self.nodes.iter() {
257            let ops_offset = if let MastNode::Block(basic_block) = mast_node {
258                basic_block_data_builder.encode_basic_block(basic_block)
259            } else {
260                0
261            };
262
263            mast_node_entries.push(MastNodeEntry::new(mast_node, ops_offset));
264            if mast_node.is_external() {
265                external_digests.push(mast_node.digest());
266            } else if !hashless {
267                node_hashes.push(mast_node.digest());
268            }
269        }
270
271        let basic_block_data = basic_block_data_builder.finalize();
272        basic_block_data.write_into(target);
273
274        for mast_node_entry in mast_node_entries {
275            mast_node_entry.write_into(target);
276        }
277
278        for digest in external_digests {
279            digest.write_into(target);
280        }
281
282        if !hashless {
283            for digest in node_hashes {
284                digest.write_into(target);
285            }
286        }
287
288        self.advice_map.write_into(target);
289    }
290}
291
292pub(super) fn write_hashless_into<W: ByteWriter>(forest: &MastForest, target: &mut W) {
293    forest.write_into_with_options(target, true);
294}
295
296/// Trusted read backing mode for read-only MAST forest access.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum MastForestReadMode {
299    /// Deserialize the full trusted cache into a materialized [`MastForest`].
300    Materialized,
301    /// Borrow complete trusted cache bytes and serve read-only data by random access.
302    WireBacked,
303}
304
305/// Read-only trusted MAST forest handle.
306#[derive(Debug)]
307pub enum MastForestReadView<'a> {
308    /// A fully materialized forest.
309    Materialized(MastForest),
310    /// A trusted wire-backed cache view.
311    WireBacked(Box<MastForestWireView<'a>>),
312}
313
314/// A trusted wire-backed view over serialized MAST forest bytes.
315///
316/// This view accepts complete payloads with hashes. It validates the header and the fixed-width
317/// structural sections needed for random access, but it does not fully materialize the forest.
318/// Hashless payloads are rejected because trusted cache bytes must be complete. Trailing payloads
319/// are rejected because debug metadata now belongs to package-owned debug sections.
320///
321/// Use this when callers need random access to roots or node metadata without deserializing the
322/// full forest. For strict trusted deserialization, use
323/// [`crate::mast::MastForest::read_from_bytes`].
324///
325/// # Examples
326///
327/// ```
328/// use miden_core::{
329///     mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastForestWireView},
330///     operations::Operation,
331///     serde::Serializable,
332/// };
333///
334/// let mut builder = DenseMastForestBuilder::new();
335/// let block_id = builder.push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])).unwrap();
336/// builder.mark_root(block_id);
337/// let forest = builder.finish().unwrap();
338///
339/// let mut bytes = Vec::new();
340/// forest.write_into(&mut bytes);
341///
342/// let view = MastForestWireView::new(&bytes).unwrap();
343/// assert_eq!(view.node_count(), forest.nodes().len());
344/// assert!(view.node_info_at(0).is_ok());
345/// ```
346#[derive(Debug)]
347pub struct MastForestWireView<'a> {
348    bytes: &'a [u8],
349    layout: ForestLayout,
350    advice_map: WireAdviceMapView<'a>,
351    resolved: OnceLockCompat<Result<ResolvedSerializedForest<'a>, DeserializationError>>,
352}
353
354impl<'a> MastForestWireView<'a> {
355    /// Creates a new view from serialized bytes.
356    ///
357    /// The input must include all node hashes. Structural parsing is
358    /// delegated to the same single-pass scanner used by reader-based deserialization paths.
359    ///
360    /// This constructor validates the header and sections needed for node/roots/random-access
361    /// metadata, indexes `AdviceMap` keys for on-demand lookup, and rejects trailing payloads.
362    ///
363    /// Treat this as a trusted cache API, not as an untrusted-validation entry point. It is
364    /// appropriate for local tools that need random access over serialized structure, but callers
365    /// handling adversarial bytes should use [`crate::mast::UntrustedMastForest`] instead.
366    ///
367    /// In particular, this constructor does **not** protect callers from untrusted-input concerns
368    /// that are enforced by [`crate::mast::UntrustedMastForest::validate`]. It does not:
369    /// - verify that serialized non-external digests match the structure they describe
370    /// - check topological ordering / forward-reference constraints
371    /// - validate basic-block batch invariants
372    /// - materialize or expose package-owned debug sections
373    ///
374    /// For strict materialized validation, use
375    /// [`crate::mast::MastForest::read_from_bytes`].
376    ///
377    /// Digest lookup follows the wire layout:
378    /// - Non-external node digests are read from the internal-hash section.
379    /// - External node digests are read from the external-digest section.
380    ///
381    /// # Examples
382    ///
383    /// ```
384    /// use miden_core::{
385    ///     mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastForestWireView},
386    ///     operations::Operation,
387    ///     serde::Serializable,
388    /// };
389    ///
390    /// let mut builder = DenseMastForestBuilder::new();
391    /// let block_id = builder.push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])).unwrap();
392    /// builder.mark_root(block_id);
393    /// let forest = builder.finish().unwrap();
394    ///
395    /// let mut bytes = Vec::new();
396    /// forest.write_into(&mut bytes);
397    ///
398    /// let view = MastForestWireView::new(&bytes).unwrap();
399    /// assert_eq!(view.node_count(), 1);
400    /// ```
401    pub fn new(bytes: &'a [u8]) -> Result<Self, DeserializationError> {
402        let mut reader = SliceReader::new(bytes);
403        let mut scanner = TrackingReader::new(&mut reader);
404        let (_flags, layout) = read_header_and_scan_layout(&mut scanner, false)?;
405        let advice_map = WireAdviceMapView::new(bytes, layout.advice_map_offset())?;
406        check_no_trailing_payload(bytes, advice_map.end_offset())?;
407        ResolvedSerializedForest::new(bytes, layout)?.validate_dense_node_order()?;
408
409        Ok(Self {
410            bytes,
411            layout,
412            advice_map,
413            resolved: OnceLockCompat::new(),
414        })
415    }
416
417    /// Returns the number of nodes in the serialized forest.
418    pub fn node_count(&self) -> usize {
419        self.layout.node_count
420    }
421
422    /// Returns the number of procedure roots in the serialized forest.
423    pub fn procedure_root_count(&self) -> usize {
424        self.layout.roots_count
425    }
426
427    /// Returns the procedure root id at the specified index.
428    ///
429    /// Returns an error if `index >= self.procedure_root_count()`.
430    pub fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
431        self.layout.read_procedure_root_at(self.bytes, index)
432    }
433
434    /// Returns the `MastNodeInfo` at the specified index.
435    ///
436    /// Returns an error if `index >= self.node_count()`.
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// use miden_core::{
442    ///     mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastForestWireView},
443    ///     operations::Operation,
444    ///     serde::Serializable,
445    /// };
446    ///
447    /// let mut builder = DenseMastForestBuilder::new();
448    /// let block_id = builder.push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])).unwrap();
449    /// builder.mark_root(block_id);
450    /// let forest = builder.finish().unwrap();
451    ///
452    /// let mut bytes = Vec::new();
453    /// forest.write_into(&mut bytes);
454    ///
455    /// let view = MastForestWireView::new(&bytes).unwrap();
456    /// assert!(view.node_info_at(0).is_ok());
457    /// ```
458    pub fn node_info_at(&self, index: usize) -> Result<MastNodeInfo, DeserializationError> {
459        Ok(MastNodeInfo::from_entry(
460            self.node_entry_at(index)?,
461            self.node_digest_at(index)?,
462        ))
463    }
464
465    /// Returns the fixed-width structural node entry at the specified index.
466    ///
467    /// Returns an error if `index >= self.node_count()`.
468    pub fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
469        self.layout.read_node_entry_at(self.bytes, index)
470    }
471
472    /// Returns the digest for the node at the specified index.
473    ///
474    /// Returns an error if `index >= self.node_count()`.
475    pub fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
476        self.resolved()?.node_digest_at(index)
477    }
478
479    /// Returns a read-only view over the serialized forest advice map.
480    pub fn advice_map(&self) -> AdviceMapView<'_> {
481        AdviceMapView::wire(&self.advice_map)
482    }
483
484    fn resolved(&self) -> Result<&ResolvedSerializedForest<'a>, DeserializationError> {
485        self.resolved
486            .get_or_init(|| ResolvedSerializedForest::new(self.bytes, self.layout))
487            .as_ref()
488            .map_err(Clone::clone)
489    }
490}
491
492fn check_no_trailing_payload(
493    bytes: &[u8],
494    debug_info_offset: usize,
495) -> Result<(), DeserializationError> {
496    let payload = bytes.get(debug_info_offset..).ok_or(DeserializationError::UnexpectedEOF)?;
497    if payload.is_empty() {
498        return Ok(());
499    }
500    Err(extra_bytes_after_mast_forest_payload_error())
501}
502
503fn extra_bytes_after_mast_forest_payload_error() -> DeserializationError {
504    DeserializationError::InvalidValue("extra bytes after MastForest payload".into())
505}
506
507impl MastForest {
508    /// Reads trusted MAST forest bytes using the requested backing mode.
509    ///
510    /// [`MastForestReadMode::Materialized`] is equivalent to [`Self::read_from_bytes`].
511    /// [`MastForestReadMode::WireBacked`] returns a trusted random-access cache view and rejects
512    /// hashless and trailing payloads because trusted cache bytes must be complete execution
513    /// payloads.
514    pub fn read_view_from_bytes(
515        bytes: &[u8],
516        mode: MastForestReadMode,
517    ) -> Result<MastForestReadView<'_>, DeserializationError> {
518        match mode {
519            MastForestReadMode::Materialized => {
520                Self::read_from_bytes(bytes).map(MastForestReadView::Materialized)
521            },
522            MastForestReadMode::WireBacked => {
523                MastForestWireView::new(bytes).map(Box::new).map(MastForestReadView::WireBacked)
524            },
525        }
526    }
527}
528
529impl MastForestView for MastForestWireView<'_> {
530    fn node_count(&self) -> usize {
531        MastForestWireView::node_count(self)
532    }
533
534    fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
535        MastForestWireView::node_entry_at(self, index)
536    }
537
538    fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
539        MastForestWireView::node_digest_at(self, index)
540    }
541
542    fn procedure_root_count(&self) -> usize {
543        MastForestWireView::procedure_root_count(self)
544    }
545
546    fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
547        MastForestWireView::procedure_root_at(self, index)
548    }
549
550    fn advice_map(&self) -> AdviceMapView<'_> {
551        MastForestWireView::advice_map(self)
552    }
553}
554
555impl MastForestView for MastForestReadView<'_> {
556    fn node_count(&self) -> usize {
557        match self {
558            MastForestReadView::Materialized(forest) => MastForestView::node_count(forest),
559            MastForestReadView::WireBacked(view) => view.node_count(),
560        }
561    }
562
563    fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
564        match self {
565            MastForestReadView::Materialized(forest) => {
566                MastForestView::node_entry_at(forest, index)
567            },
568            MastForestReadView::WireBacked(view) => view.node_entry_at(index),
569        }
570    }
571
572    fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
573        match self {
574            MastForestReadView::Materialized(forest) => {
575                MastForestView::node_digest_at(forest, index)
576            },
577            MastForestReadView::WireBacked(view) => view.node_digest_at(index),
578        }
579    }
580
581    fn procedure_root_count(&self) -> usize {
582        match self {
583            MastForestReadView::Materialized(forest) => {
584                MastForestView::procedure_root_count(forest)
585            },
586            MastForestReadView::WireBacked(view) => view.procedure_root_count(),
587        }
588    }
589
590    fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
591        match self {
592            MastForestReadView::Materialized(forest) => {
593                MastForestView::procedure_root_at(forest, index)
594            },
595            MastForestReadView::WireBacked(view) => view.procedure_root_at(index),
596        }
597    }
598
599    fn advice_map(&self) -> AdviceMapView<'_> {
600        match self {
601            MastForestReadView::Materialized(forest) => MastForestView::advice_map(forest),
602            MastForestReadView::WireBacked(view) => view.advice_map(),
603        }
604    }
605}
606
607impl MastForestView for MastForest {
608    fn node_count(&self) -> usize {
609        self.nodes.len()
610    }
611
612    fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
613        let node = self.nodes.as_slice().get(index).ok_or_else(|| {
614            DeserializationError::InvalidValue(format!("node index {index} out of bounds"))
615        })?;
616        let ops_offset = if matches!(node, MastNode::Block(_)) {
617            basic_block_offset_for_node_index(self.nodes.as_slice(), index)?
618        } else {
619            0
620        };
621
622        Ok(MastNodeEntry::new(node, ops_offset))
623    }
624
625    fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
626        self.nodes.as_slice().get(index).map(MastNode::digest).ok_or_else(|| {
627            DeserializationError::InvalidValue(format!("node index {index} out of bounds"))
628        })
629    }
630
631    fn procedure_root_count(&self) -> usize {
632        self.roots.len()
633    }
634
635    fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
636        self.roots.get(index).copied().ok_or_else(|| {
637            DeserializationError::InvalidValue(format!(
638                "root index {} out of bounds for {} roots",
639                index,
640                self.roots.len()
641            ))
642        })
643    }
644
645    fn advice_map(&self) -> AdviceMapView<'_> {
646        AdviceMapView::materialized(&self.advice_map)
647    }
648}
649
650// TEST HELPERS
651// ================================================================================================
652
653#[cfg(test)]
654impl MastForestWireView<'_> {
655    fn debug_info_offset(&self) -> usize {
656        self.advice_map.end_offset()
657    }
658
659    fn node_entry_offset(&self) -> usize {
660        self.layout.node_entry_offset()
661    }
662
663    fn external_digest_offset(&self) -> usize {
664        self.layout.external_digest_offset()
665    }
666
667    fn node_hash_offset(&self) -> Option<usize> {
668        self.layout.node_hash_offset()
669    }
670
671    fn digest_slot_at(&self, index: usize) -> usize {
672        self.resolved()
673            .expect("digest slots should be readable for a valid serialized view")
674            .digest_slot_at(index)
675    }
676}
677
678#[cfg(test)]
679fn read_u8_at(bytes: &[u8], offset: &mut usize) -> Result<u8, DeserializationError> {
680    read_slice_at(bytes, offset, 1).map(|slice| slice[0])
681}
682
683#[cfg(test)]
684fn read_array_at<const N: usize>(
685    bytes: &[u8],
686    offset: &mut usize,
687) -> Result<[u8; N], DeserializationError> {
688    let slice = read_slice_at(bytes, offset, N)?;
689    let mut result = [0u8; N];
690    result.copy_from_slice(slice);
691    Ok(result)
692}
693
694#[cfg(test)]
695fn read_slice_at<'a>(
696    bytes: &'a [u8],
697    offset: &mut usize,
698    len: usize,
699) -> Result<&'a [u8], DeserializationError> {
700    let end = offset
701        .checked_add(len)
702        .ok_or_else(|| DeserializationError::InvalidValue("offset overflow".to_string()))?;
703    if end > bytes.len() {
704        return Err(DeserializationError::UnexpectedEOF);
705    }
706    let slice = &bytes[*offset..end];
707    *offset = end;
708    Ok(slice)
709}
710
711// NOTE: Mirrors ByteReader::read_usize (vint64) decoding to preserve wire compatibility.
712#[cfg(test)]
713fn read_usize_at(bytes: &[u8], offset: &mut usize) -> Result<usize, DeserializationError> {
714    if *offset >= bytes.len() {
715        return Err(DeserializationError::UnexpectedEOF);
716    }
717    let first_byte = bytes[*offset];
718    let length = first_byte.trailing_zeros() as usize + 1;
719
720    let result = if length == 9 {
721        let _marker = read_u8_at(bytes, offset)?;
722        let value = read_array_at::<8>(bytes, offset)?;
723        u64::from_le_bytes(value)
724    } else {
725        let mut encoded = [0u8; 8];
726        let value = read_slice_at(bytes, offset, length)?;
727        encoded[..length].copy_from_slice(value);
728        u64::from_le_bytes(encoded) >> length
729    };
730
731    if result > usize::MAX as u64 {
732        return Err(DeserializationError::InvalidValue(format!(
733            "Encoded value must be less than {}, but {} was provided",
734            usize::MAX,
735            result
736        )));
737    }
738
739    Ok(result as usize)
740}
741
742impl Deserializable for MastForest {
743    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
744        let (_flags, forest) = decode_from_reader(source, false)?;
745        forest.into_materialized()
746    }
747
748    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
749        let budget = bytes.len().saturating_mul(TRUSTED_BYTE_READ_BUDGET_MULTIPLIER);
750        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
751        let forest = Self::read_from(&mut reader)?;
752        if reader.has_more_bytes() {
753            return Err(extra_bytes_after_mast_forest_payload_error());
754        }
755        Ok(forest)
756    }
757}
758
759impl super::UntrustedMastForest {
760    pub(super) fn into_materialized(self) -> Result<MastForest, DeserializationError> {
761        let resolved = if let Some(allocation_budget) = self.remaining_allocation_budget {
762            ResolvedSerializedForest::new_with_allocation_budget(
763                &self.bytes,
764                self.layout,
765                allocation_budget,
766            )?
767        } else {
768            ResolvedSerializedForest::new(&self.bytes, self.layout)?
769        };
770
771        resolved.materialize(self.advice_map)
772    }
773}
774
775pub(super) fn read_untrusted_with_flags<R: ByteReader>(
776    source: &mut R,
777) -> Result<(super::UntrustedMastForest, u8), DeserializationError> {
778    let (flags, forest) = decode_from_reader(source, true)?;
779    log_untrusted_overspecification(flags);
780    Ok((forest, flags.bits()))
781}
782
783pub(super) fn read_untrusted_with_flags_and_allocation_budget<R: ByteReader>(
784    source: &mut R,
785    allocation_budget: usize,
786) -> Result<(super::UntrustedMastForest, u8), DeserializationError> {
787    let (flags, forest) = decode_from_reader_inner(source, true, Some(allocation_budget))?;
788    log_untrusted_overspecification(flags);
789    Ok((forest, flags.bits()))
790}
791
792fn log_untrusted_overspecification(flags: WireFlags) {
793    if !flags.is_hashless() {
794        log::error!(
795            "UntrustedMastForest expected HASHLESS input; supplied artifact includes wire node hashes, and validation will recompute them and require them to match"
796        );
797    }
798}
799
800fn decode_from_reader<R: ByteReader>(
801    source: &mut R,
802    allow_hashless: bool,
803) -> Result<(WireFlags, super::UntrustedMastForest), DeserializationError> {
804    decode_from_reader_inner(source, allow_hashless, None)
805}
806
807fn decode_from_reader_inner<R: ByteReader>(
808    source: &mut R,
809    allow_hashless: bool,
810    remaining_allocation_budget: Option<usize>,
811) -> Result<(WireFlags, super::UntrustedMastForest), DeserializationError> {
812    let mut recording = TrackingReader::new_recording(source);
813    let (flags, layout) = read_header_and_scan_layout(&mut recording, allow_hashless)?;
814    debug_assert_eq!(recording.offset(), layout.advice_map_offset());
815
816    let advice_map = AdviceMap::read_from(&mut recording)?;
817    Ok((
818        flags,
819        super::UntrustedMastForest {
820            bytes: recording.into_recorded(),
821            layout,
822            advice_map,
823            remaining_allocation_budget,
824        },
825    ))
826}
827
828pub(super) fn reserve_allocation<T>(
829    remaining_budget: &mut usize,
830    count: usize,
831    label: &str,
832) -> Result<(), DeserializationError> {
833    let bytes_needed = count
834        .checked_mul(size_of::<T>())
835        .ok_or_else(|| DeserializationError::InvalidValue(format!("{label} size overflow")))?;
836    if bytes_needed > *remaining_budget {
837        return Err(DeserializationError::InvalidValue(format!(
838            "{label} requires {bytes_needed} bytes, exceeding the remaining untrusted allocation budget of {} bytes",
839            *remaining_budget
840        )));
841    }
842
843    *remaining_budget -= bytes_needed;
844    Ok(())
845}
846
847pub(super) fn default_untrusted_allocation_budget(bytes_len: usize) -> usize {
848    bytes_len.saturating_mul(DEFAULT_UNTRUSTED_ALLOCATION_BUDGET_MULTIPLIER)
849}
850
851// UNTRUSTED DESERIALIZATION
852// ================================================================================================
853
854impl Deserializable for super::UntrustedMastForest {
855    /// Deserializes an [`super::UntrustedMastForest`] from a byte reader.
856    ///
857    /// Note: This method does not apply budgeting. For untrusted input, prefer using
858    /// [`read_from_bytes`](Self::read_from_bytes) which applies budgeted deserialization.
859    ///
860    /// After deserialization, callers should use [`super::UntrustedMastForest::validate()`]
861    /// to verify structural integrity and recompute all node hashes before using
862    /// the forest.
863    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
864        read_untrusted_with_flags(source).map(|(forest, _flags)| forest)
865    }
866
867    /// Deserializes an [`super::UntrustedMastForest`] from bytes using budgeted deserialization.
868    ///
869    /// This method uses the default untrusted wire/validation budget from
870    /// [`super::UntrustedMastForest::read_from_bytes`].
871    ///
872    /// After deserialization, callers should use [`super::UntrustedMastForest::validate()`]
873    /// to verify structural integrity and recompute all node hashes before using
874    /// the forest.
875    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
876        super::UntrustedMastForest::read_from_bytes(bytes)
877    }
878}