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