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 fn write_into_with_options<W: ByteWriter>(&self, target: &mut W, hashless: bool) {
226 let mut basic_block_data_builder = BasicBlockDataBuilder::new();
227
228 // magic & flags
229 target.write_bytes(MAGIC);
230 let flags = if hashless { FLAG_HASHLESS } else { 0 };
231 target.write_u8(flags);
232
233 // version
234 target.write_bytes(&VERSION);
235
236 // header counts
237 let node_count = self.nodes.len();
238 let external_node_count = self.nodes.iter().filter(|node| node.is_external()).count();
239 let internal_node_count = node_count - external_node_count;
240 target.write_usize(internal_node_count);
241 target.write_usize(external_node_count);
242
243 // roots
244 let roots: Vec<u32> = self.roots.iter().copied().map(u32::from).collect();
245 roots.write_into(target);
246
247 let mut mast_node_entries = Vec::with_capacity(self.nodes.len());
248 let mut external_digests = Vec::with_capacity(external_node_count);
249 let mut node_hashes = Vec::new();
250
251 for mast_node in self.nodes.iter() {
252 let ops_offset = if let MastNode::Block(basic_block) = mast_node {
253 basic_block_data_builder.encode_basic_block(basic_block)
254 } else {
255 0
256 };
257
258 mast_node_entries.push(MastNodeEntry::new(mast_node, ops_offset));
259 if mast_node.is_external() {
260 external_digests.push(mast_node.digest());
261 } else if !hashless {
262 node_hashes.push(mast_node.digest());
263 }
264 }
265
266 let basic_block_data = basic_block_data_builder.finalize();
267 basic_block_data.write_into(target);
268
269 for mast_node_entry in mast_node_entries {
270 mast_node_entry.write_into(target);
271 }
272
273 for digest in external_digests {
274 digest.write_into(target);
275 }
276
277 if !hashless {
278 for digest in node_hashes {
279 digest.write_into(target);
280 }
281 }
282
283 self.advice_map.write_into(target);
284 }
285}
286
287pub(super) fn write_hashless_into<W: ByteWriter>(forest: &MastForest, target: &mut W) {
288 forest.write_into_with_options(target, true);
289}
290
291/// Trusted read backing mode for read-only MAST forest access.
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum MastForestReadMode {
294 /// Deserialize the full trusted cache into a materialized [`MastForest`].
295 Materialized,
296 /// Borrow complete trusted cache bytes and serve read-only data by random access.
297 WireBacked,
298}
299
300/// Read-only trusted MAST forest handle.
301#[derive(Debug)]
302pub enum MastForestReadView<'a> {
303 /// A fully materialized forest.
304 Materialized(MastForest),
305 /// A trusted wire-backed cache view.
306 WireBacked(Box<MastForestWireView<'a>>),
307}
308
309/// A trusted wire-backed view over serialized MAST forest bytes.
310///
311/// This view accepts complete payloads with hashes. It validates the header and the fixed-width
312/// structural sections needed for random access, but it does not fully materialize the forest.
313/// Hashless payloads are rejected because trusted cache bytes must be complete. Trailing payloads
314/// are rejected because debug metadata now belongs to package-owned debug sections.
315///
316/// Use this when callers need random access to roots or node metadata without deserializing the
317/// full forest. For strict trusted deserialization, use
318/// [`crate::mast::MastForest::read_from_bytes`].
319///
320/// # Examples
321///
322/// ```
323/// use miden_core::{
324/// mast::{BasicBlockNodeBuilder, MastForest, MastForestContributor, MastForestWireView},
325/// operations::Operation,
326/// serde::Serializable,
327/// };
328///
329/// let mut forest = MastForest::new();
330/// let block_id = BasicBlockNodeBuilder::new(vec![Operation::Add])
331/// .add_to_forest(&mut forest)
332/// .unwrap();
333/// forest.make_root(block_id);
334///
335/// let mut bytes = Vec::new();
336/// forest.write_into(&mut bytes);
337///
338/// let view = MastForestWireView::new(&bytes).unwrap();
339/// assert_eq!(view.node_count(), forest.nodes().len());
340/// assert!(view.node_info_at(0).is_ok());
341/// ```
342#[derive(Debug)]
343pub struct MastForestWireView<'a> {
344 bytes: &'a [u8],
345 layout: ForestLayout,
346 advice_map: WireAdviceMapView<'a>,
347 resolved: OnceLockCompat<Result<ResolvedSerializedForest<'a>, DeserializationError>>,
348}
349
350impl<'a> MastForestWireView<'a> {
351 /// Creates a new view from serialized bytes.
352 ///
353 /// The input must include all node hashes. Structural parsing is
354 /// delegated to the same single-pass scanner used by reader-based deserialization paths.
355 ///
356 /// This constructor validates the header and sections needed for node/roots/random-access
357 /// metadata, indexes `AdviceMap` keys for on-demand lookup, and rejects trailing payloads.
358 ///
359 /// Treat this as a trusted cache API, not as an untrusted-validation entry point. It is
360 /// appropriate for local tools that need random access over serialized structure, but callers
361 /// handling adversarial bytes should use [`crate::mast::UntrustedMastForest`] instead.
362 ///
363 /// In particular, this constructor does **not** protect callers from untrusted-input concerns
364 /// that are enforced by [`crate::mast::UntrustedMastForest::validate`]. It does not:
365 /// - verify that serialized non-external digests match the structure they describe
366 /// - check topological ordering / forward-reference constraints
367 /// - validate basic-block batch invariants
368 /// - materialize or expose package-owned debug sections
369 ///
370 /// For strict materialized validation, use
371 /// [`crate::mast::MastForest::read_from_bytes`].
372 ///
373 /// Digest lookup follows the wire layout:
374 /// - Non-external node digests are read from the internal-hash section.
375 /// - External node digests are read from the external-digest section.
376 ///
377 /// # Examples
378 ///
379 /// ```
380 /// use miden_core::{
381 /// mast::{BasicBlockNodeBuilder, MastForest, MastForestContributor, MastForestWireView},
382 /// operations::Operation,
383 /// serde::Serializable,
384 /// };
385 ///
386 /// let mut forest = MastForest::new();
387 /// let block_id = BasicBlockNodeBuilder::new(vec![Operation::Add])
388 /// .add_to_forest(&mut forest)
389 /// .unwrap();
390 /// forest.make_root(block_id);
391 ///
392 /// let mut bytes = Vec::new();
393 /// forest.write_into(&mut bytes);
394 ///
395 /// let view = MastForestWireView::new(&bytes).unwrap();
396 /// assert_eq!(view.node_count(), 1);
397 /// ```
398 pub fn new(bytes: &'a [u8]) -> Result<Self, DeserializationError> {
399 let mut reader = SliceReader::new(bytes);
400 let mut scanner = TrackingReader::new(&mut reader);
401 let (_flags, layout) = read_header_and_scan_layout(&mut scanner, false)?;
402 let advice_map = WireAdviceMapView::new(bytes, layout.advice_map_offset())?;
403 check_no_trailing_payload(bytes, advice_map.end_offset())?;
404
405 Ok(Self {
406 bytes,
407 layout,
408 advice_map,
409 resolved: OnceLockCompat::new(),
410 })
411 }
412
413 /// Returns the number of nodes in the serialized forest.
414 pub fn node_count(&self) -> usize {
415 self.layout.node_count
416 }
417
418 /// Returns the number of procedure roots in the serialized forest.
419 pub fn procedure_root_count(&self) -> usize {
420 self.layout.roots_count
421 }
422
423 /// Returns the procedure root id at the specified index.
424 ///
425 /// Returns an error if `index >= self.procedure_root_count()`.
426 pub fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
427 self.layout.read_procedure_root_at(self.bytes, index)
428 }
429
430 /// Returns the `MastNodeInfo` at the specified index.
431 ///
432 /// Returns an error if `index >= self.node_count()`.
433 ///
434 /// # Examples
435 ///
436 /// ```
437 /// use miden_core::{
438 /// mast::{BasicBlockNodeBuilder, MastForest, MastForestContributor, MastForestWireView},
439 /// operations::Operation,
440 /// serde::Serializable,
441 /// };
442 ///
443 /// let mut forest = MastForest::new();
444 /// let block_id = BasicBlockNodeBuilder::new(vec![Operation::Add])
445 /// .add_to_forest(&mut forest)
446 /// .unwrap();
447 /// forest.make_root(block_id);
448 ///
449 /// let mut bytes = Vec::new();
450 /// forest.write_into(&mut bytes);
451 ///
452 /// let view = MastForestWireView::new(&bytes).unwrap();
453 /// assert!(view.node_info_at(0).is_ok());
454 /// ```
455 pub fn node_info_at(&self, index: usize) -> Result<MastNodeInfo, DeserializationError> {
456 Ok(MastNodeInfo::from_entry(
457 self.node_entry_at(index)?,
458 self.node_digest_at(index)?,
459 ))
460 }
461
462 /// Returns the fixed-width structural node entry at the specified index.
463 ///
464 /// Returns an error if `index >= self.node_count()`.
465 pub fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
466 self.layout.read_node_entry_at(self.bytes, index)
467 }
468
469 /// Returns the digest for the node at the specified index.
470 ///
471 /// Returns an error if `index >= self.node_count()`.
472 pub fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
473 self.resolved()?.node_digest_at(index)
474 }
475
476 /// Returns a read-only view over the serialized forest advice map.
477 pub fn advice_map(&self) -> AdviceMapView<'_> {
478 AdviceMapView::wire(&self.advice_map)
479 }
480
481 fn resolved(&self) -> Result<&ResolvedSerializedForest<'a>, DeserializationError> {
482 self.resolved
483 .get_or_init(|| ResolvedSerializedForest::new(self.bytes, self.layout))
484 .as_ref()
485 .map_err(Clone::clone)
486 }
487}
488
489fn check_no_trailing_payload(
490 bytes: &[u8],
491 debug_info_offset: usize,
492) -> Result<(), DeserializationError> {
493 let payload = bytes.get(debug_info_offset..).ok_or(DeserializationError::UnexpectedEOF)?;
494 if payload.is_empty() {
495 return Ok(());
496 }
497 Err(extra_bytes_after_mast_forest_payload_error())
498}
499
500fn extra_bytes_after_mast_forest_payload_error() -> DeserializationError {
501 DeserializationError::InvalidValue("extra bytes after MastForest payload".into())
502}
503
504impl MastForest {
505 /// Reads trusted MAST forest bytes using the requested backing mode.
506 ///
507 /// [`MastForestReadMode::Materialized`] is equivalent to [`Self::read_from_bytes`].
508 /// [`MastForestReadMode::WireBacked`] returns a trusted random-access cache view and rejects
509 /// hashless and trailing payloads because trusted cache bytes must be complete execution
510 /// payloads.
511 pub fn read_view_from_bytes(
512 bytes: &[u8],
513 mode: MastForestReadMode,
514 ) -> Result<MastForestReadView<'_>, DeserializationError> {
515 match mode {
516 MastForestReadMode::Materialized => {
517 Self::read_from_bytes(bytes).map(MastForestReadView::Materialized)
518 },
519 MastForestReadMode::WireBacked => {
520 MastForestWireView::new(bytes).map(Box::new).map(MastForestReadView::WireBacked)
521 },
522 }
523 }
524}
525
526impl MastForestView for MastForestWireView<'_> {
527 fn node_count(&self) -> usize {
528 MastForestWireView::node_count(self)
529 }
530
531 fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
532 MastForestWireView::node_entry_at(self, index)
533 }
534
535 fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
536 MastForestWireView::node_digest_at(self, index)
537 }
538
539 fn procedure_root_count(&self) -> usize {
540 MastForestWireView::procedure_root_count(self)
541 }
542
543 fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
544 MastForestWireView::procedure_root_at(self, index)
545 }
546
547 fn advice_map(&self) -> AdviceMapView<'_> {
548 MastForestWireView::advice_map(self)
549 }
550}
551
552impl MastForestView for MastForestReadView<'_> {
553 fn node_count(&self) -> usize {
554 match self {
555 MastForestReadView::Materialized(forest) => MastForestView::node_count(forest),
556 MastForestReadView::WireBacked(view) => view.node_count(),
557 }
558 }
559
560 fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
561 match self {
562 MastForestReadView::Materialized(forest) => {
563 MastForestView::node_entry_at(forest, index)
564 },
565 MastForestReadView::WireBacked(view) => view.node_entry_at(index),
566 }
567 }
568
569 fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
570 match self {
571 MastForestReadView::Materialized(forest) => {
572 MastForestView::node_digest_at(forest, index)
573 },
574 MastForestReadView::WireBacked(view) => view.node_digest_at(index),
575 }
576 }
577
578 fn procedure_root_count(&self) -> usize {
579 match self {
580 MastForestReadView::Materialized(forest) => {
581 MastForestView::procedure_root_count(forest)
582 },
583 MastForestReadView::WireBacked(view) => view.procedure_root_count(),
584 }
585 }
586
587 fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
588 match self {
589 MastForestReadView::Materialized(forest) => {
590 MastForestView::procedure_root_at(forest, index)
591 },
592 MastForestReadView::WireBacked(view) => view.procedure_root_at(index),
593 }
594 }
595
596 fn advice_map(&self) -> AdviceMapView<'_> {
597 match self {
598 MastForestReadView::Materialized(forest) => MastForestView::advice_map(forest),
599 MastForestReadView::WireBacked(view) => view.advice_map(),
600 }
601 }
602}
603
604impl MastForestView for MastForest {
605 fn node_count(&self) -> usize {
606 self.nodes.len()
607 }
608
609 fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
610 let node = self.nodes.as_slice().get(index).ok_or_else(|| {
611 DeserializationError::InvalidValue(format!("node index {index} out of bounds"))
612 })?;
613 let ops_offset = if matches!(node, MastNode::Block(_)) {
614 basic_block_offset_for_node_index(self.nodes.as_slice(), index)?
615 } else {
616 0
617 };
618
619 Ok(MastNodeEntry::new(node, ops_offset))
620 }
621
622 fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
623 self.nodes.as_slice().get(index).map(MastNode::digest).ok_or_else(|| {
624 DeserializationError::InvalidValue(format!("node index {index} out of bounds"))
625 })
626 }
627
628 fn procedure_root_count(&self) -> usize {
629 self.roots.len()
630 }
631
632 fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
633 self.roots.get(index).copied().ok_or_else(|| {
634 DeserializationError::InvalidValue(format!(
635 "root index {} out of bounds for {} roots",
636 index,
637 self.roots.len()
638 ))
639 })
640 }
641
642 fn advice_map(&self) -> AdviceMapView<'_> {
643 AdviceMapView::materialized(&self.advice_map)
644 }
645}
646
647// TEST HELPERS
648// ================================================================================================
649
650#[cfg(test)]
651impl MastForestWireView<'_> {
652 fn debug_info_offset(&self) -> usize {
653 self.advice_map.end_offset()
654 }
655
656 fn node_entry_offset(&self) -> usize {
657 self.layout.node_entry_offset()
658 }
659
660 fn external_digest_offset(&self) -> usize {
661 self.layout.external_digest_offset()
662 }
663
664 fn node_hash_offset(&self) -> Option<usize> {
665 self.layout.node_hash_offset()
666 }
667
668 fn digest_slot_at(&self, index: usize) -> usize {
669 self.resolved()
670 .expect("digest slots should be readable for a valid serialized view")
671 .digest_slot_at(index)
672 }
673}
674
675#[cfg(test)]
676fn read_u8_at(bytes: &[u8], offset: &mut usize) -> Result<u8, DeserializationError> {
677 read_slice_at(bytes, offset, 1).map(|slice| slice[0])
678}
679
680#[cfg(test)]
681fn read_array_at<const N: usize>(
682 bytes: &[u8],
683 offset: &mut usize,
684) -> Result<[u8; N], DeserializationError> {
685 let slice = read_slice_at(bytes, offset, N)?;
686 let mut result = [0u8; N];
687 result.copy_from_slice(slice);
688 Ok(result)
689}
690
691#[cfg(test)]
692fn read_slice_at<'a>(
693 bytes: &'a [u8],
694 offset: &mut usize,
695 len: usize,
696) -> Result<&'a [u8], DeserializationError> {
697 let end = offset
698 .checked_add(len)
699 .ok_or_else(|| DeserializationError::InvalidValue("offset overflow".to_string()))?;
700 if end > bytes.len() {
701 return Err(DeserializationError::UnexpectedEOF);
702 }
703 let slice = &bytes[*offset..end];
704 *offset = end;
705 Ok(slice)
706}
707
708// NOTE: Mirrors ByteReader::read_usize (vint64) decoding to preserve wire compatibility.
709#[cfg(test)]
710fn read_usize_at(bytes: &[u8], offset: &mut usize) -> Result<usize, DeserializationError> {
711 if *offset >= bytes.len() {
712 return Err(DeserializationError::UnexpectedEOF);
713 }
714 let first_byte = bytes[*offset];
715 let length = first_byte.trailing_zeros() as usize + 1;
716
717 let result = if length == 9 {
718 let _marker = read_u8_at(bytes, offset)?;
719 let value = read_array_at::<8>(bytes, offset)?;
720 u64::from_le_bytes(value)
721 } else {
722 let mut encoded = [0u8; 8];
723 let value = read_slice_at(bytes, offset, length)?;
724 encoded[..length].copy_from_slice(value);
725 u64::from_le_bytes(encoded) >> length
726 };
727
728 if result > usize::MAX as u64 {
729 return Err(DeserializationError::InvalidValue(format!(
730 "Encoded value must be less than {}, but {} was provided",
731 usize::MAX,
732 result
733 )));
734 }
735
736 Ok(result as usize)
737}
738
739impl Deserializable for MastForest {
740 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
741 let (_flags, forest) = decode_from_reader(source, false)?;
742 forest.into_materialized()
743 }
744
745 fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
746 let budget = bytes.len().saturating_mul(TRUSTED_BYTE_READ_BUDGET_MULTIPLIER);
747 let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
748 let forest = Self::read_from(&mut reader)?;
749 if reader.has_more_bytes() {
750 return Err(extra_bytes_after_mast_forest_payload_error());
751 }
752 Ok(forest)
753 }
754}
755
756impl super::UntrustedMastForest {
757 pub(super) fn into_materialized(self) -> Result<MastForest, DeserializationError> {
758 let resolved = if let Some(allocation_budget) = self.remaining_allocation_budget {
759 ResolvedSerializedForest::new_with_allocation_budget(
760 &self.bytes,
761 self.layout,
762 allocation_budget,
763 )?
764 } else {
765 ResolvedSerializedForest::new(&self.bytes, self.layout)?
766 };
767
768 resolved.materialize(self.advice_map)
769 }
770}
771
772pub(super) fn read_untrusted_with_flags<R: ByteReader>(
773 source: &mut R,
774) -> Result<(super::UntrustedMastForest, u8), DeserializationError> {
775 let (flags, forest) = decode_from_reader(source, true)?;
776 log_untrusted_overspecification(flags);
777 Ok((forest, flags.bits()))
778}
779
780pub(super) fn read_untrusted_with_flags_and_allocation_budget<R: ByteReader>(
781 source: &mut R,
782 allocation_budget: usize,
783) -> Result<(super::UntrustedMastForest, u8), DeserializationError> {
784 let (flags, forest) = decode_from_reader_inner(source, true, Some(allocation_budget))?;
785 log_untrusted_overspecification(flags);
786 Ok((forest, flags.bits()))
787}
788
789fn log_untrusted_overspecification(flags: WireFlags) {
790 if !flags.is_hashless() {
791 log::error!(
792 "UntrustedMastForest expected HASHLESS input; supplied artifact includes wire node hashes, and validation will recompute them and require them to match"
793 );
794 }
795}
796
797fn decode_from_reader<R: ByteReader>(
798 source: &mut R,
799 allow_hashless: bool,
800) -> Result<(WireFlags, super::UntrustedMastForest), DeserializationError> {
801 decode_from_reader_inner(source, allow_hashless, None)
802}
803
804fn decode_from_reader_inner<R: ByteReader>(
805 source: &mut R,
806 allow_hashless: bool,
807 remaining_allocation_budget: Option<usize>,
808) -> Result<(WireFlags, super::UntrustedMastForest), DeserializationError> {
809 let mut recording = TrackingReader::new_recording(source);
810 let (flags, layout) = read_header_and_scan_layout(&mut recording, allow_hashless)?;
811 debug_assert_eq!(recording.offset(), layout.advice_map_offset());
812
813 let advice_map = AdviceMap::read_from(&mut recording)?;
814 Ok((
815 flags,
816 super::UntrustedMastForest {
817 bytes: recording.into_recorded(),
818 layout,
819 advice_map,
820 remaining_allocation_budget,
821 },
822 ))
823}
824
825pub(super) fn reserve_allocation<T>(
826 remaining_budget: &mut usize,
827 count: usize,
828 label: &str,
829) -> Result<(), DeserializationError> {
830 let bytes_needed = count
831 .checked_mul(size_of::<T>())
832 .ok_or_else(|| DeserializationError::InvalidValue(format!("{label} size overflow")))?;
833 if bytes_needed > *remaining_budget {
834 return Err(DeserializationError::InvalidValue(format!(
835 "{label} requires {bytes_needed} bytes, exceeding the remaining untrusted allocation budget of {} bytes",
836 *remaining_budget
837 )));
838 }
839
840 *remaining_budget -= bytes_needed;
841 Ok(())
842}
843
844pub(super) fn default_untrusted_allocation_budget(bytes_len: usize) -> usize {
845 bytes_len.saturating_mul(DEFAULT_UNTRUSTED_ALLOCATION_BUDGET_MULTIPLIER)
846}
847
848// UNTRUSTED DESERIALIZATION
849// ================================================================================================
850
851impl Deserializable for super::UntrustedMastForest {
852 /// Deserializes an [`super::UntrustedMastForest`] from a byte reader.
853 ///
854 /// Note: This method does not apply budgeting. For untrusted input, prefer using
855 /// [`read_from_bytes`](Self::read_from_bytes) which applies budgeted deserialization.
856 ///
857 /// After deserialization, callers should use [`super::UntrustedMastForest::validate()`]
858 /// to verify structural integrity and recompute all node hashes before using
859 /// the forest.
860 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
861 read_untrusted_with_flags(source).map(|(forest, _flags)| forest)
862 }
863
864 /// Deserializes an [`super::UntrustedMastForest`] from bytes using budgeted deserialization.
865 ///
866 /// This method uses the default untrusted wire/validation budget from
867 /// [`super::UntrustedMastForest::read_from_bytes`].
868 ///
869 /// After deserialization, callers should use [`super::UntrustedMastForest::validate()`]
870 /// to verify structural integrity and recompute all node hashes before using
871 /// the forest.
872 fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
873 super::UntrustedMastForest::read_from_bytes(bytes)
874 }
875}