Skip to main content

miden_core/mast/
untrusted.rs

1use alloc::vec::Vec;
2use core::panic::Location;
3
4use super::{AdviceMap, MastForest, MastForestError, serialization};
5use crate::serde::{BudgetedReader, ByteReader, DeserializationError, SliceReader};
6
7/// A [`MastForest`] deserialized from untrusted input that has not yet been validated.
8///
9/// This type wraps a serialized-backed, decoded MAST representation that has not had its node
10/// hashes verified. Before using the forest, callers must call [`validate()`](Self::validate) to
11/// materialize and verify structural integrity and node hashes.
12///
13/// # Usage
14///
15/// ```ignore
16/// // Deserialize from untrusted bytes
17/// let untrusted = UntrustedMastForest::read_from_bytes(&bytes)?;
18///
19/// // Validate structure and hashes
20/// let forest = untrusted.validate()?;
21///
22/// // Now safe to use
23/// let root = forest.procedure_roots()[0];
24/// ```
25///
26/// # Security
27///
28/// This type exists to provide type-level safety for untrusted deserialization. The validation
29/// performed by [`validate()`](Self::validate) includes:
30///
31/// 1. **Structural validation**: Checks that basic block batch invariants are satisfied.
32/// 2. **Topological ordering**: Verifies that all node references point to nodes that appear
33///    earlier in the forest (no forward references).
34/// 3. **Hash recomputation**: Recomputes the digest for every node and verifies it matches the
35///    stored digest.
36#[derive(Debug, Clone)]
37pub struct UntrustedMastForest {
38    pub(super) bytes: Vec<u8>,
39    pub(super) layout: serialization::ForestLayout,
40    pub(super) advice_map: AdviceMap,
41    pub(super) remaining_allocation_budget: Option<usize>,
42}
43
44/// Options for reading an [`UntrustedMastForest`] from bytes.
45#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
46pub struct UntrustedMastForestReadOptions {
47    wire_byte_budget: Option<usize>,
48    validation_allocation_budget: Option<usize>,
49}
50
51impl UntrustedMastForestReadOptions {
52    /// Creates options that use the default untrusted budgets.
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Sets the maximum number of serialized bytes consumed while parsing wire data.
58    pub fn with_wire_byte_budget(mut self, budget: usize) -> Self {
59        self.wire_byte_budget = Some(budget);
60        self
61    }
62
63    #[cfg(test)]
64    pub(crate) fn with_validation_allocation_budget(mut self, budget: usize) -> Self {
65        self.validation_allocation_budget = Some(budget);
66        self
67    }
68
69    fn wire_byte_budget(self, bytes_len: usize) -> usize {
70        self.wire_byte_budget.unwrap_or(bytes_len)
71    }
72
73    fn validation_allocation_budget(self, wire_byte_budget: usize) -> usize {
74        self.validation_allocation_budget
75            .unwrap_or_else(|| serialization::default_untrusted_allocation_budget(wire_byte_budget))
76    }
77}
78
79impl UntrustedMastForest {
80    /// Deserializes an [`UntrustedMastForest`] from a byte reader.
81    ///
82    /// Note: This method does not apply budgeting. For untrusted bytes, prefer
83    /// [`read_from_bytes`](Self::read_from_bytes) or
84    /// [`read_from_bytes_with_options`](Self::read_from_bytes_with_options).
85    #[track_caller]
86    pub fn read_from_reader<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
87        let caller = Location::caller();
88        serialization::read_untrusted_with_flags_and_caller(source, caller)
89            .map(|(forest, _flags)| forest)
90    }
91
92    /// Validates the forest by checking structural invariants and recomputing all node hashes.
93    ///
94    /// This method performs a complete validation of the deserialized forest:
95    ///
96    /// 1. If wire node hashes are present, recomputes all non-external node hashes and requires
97    ///    them to match the serialized digests.
98    /// 2. If the payload is hashless, uses the digests rebuilt during materialization.
99    /// 3. Validates structural invariants and topological ordering.
100    ///
101    /// # Returns
102    ///
103    /// - `Ok(MastForest)` if validation succeeds
104    /// - `Err(MastForestError)` with details about the first validation failure
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if:
109    /// - Deferred materialization from serialized form fails ([`MastForestError::Deserialization`])
110    /// - Any basic block has invalid batch structure ([`MastForestError::InvalidBatchPadding`])
111    /// - Any node references a child that appears later in the forest
112    ///   ([`MastForestError::ForwardReference`])
113    /// - Any non-external wire digest does not match the recomputed digest
114    ///   ([`MastForestError::HashMismatch`])
115    /// - Any node's digest cannot be recomputed because structural validation fails first
116    ///
117    /// Security convention:
118    /// - Hashless payloads rebuild non-external digests from structure during materialization.
119    /// - If wire node hashes are present, validation recomputes them and requires them to match.
120    /// - External node digests are marshaled as opaque values and are not semantically resolved
121    ///   here.
122    pub fn validate(self) -> Result<MastForest, MastForestError> {
123        let is_hashless = self.layout.is_hashless();
124        let forest = self.into_materialized().map_err(MastForestError::Deserialization)?;
125
126        // Step 1: Validate over-specified wire hashes instead of silently rewriting them.
127        if !is_hashless {
128            forest.validate_node_hashes()?;
129        }
130
131        // Step 2: Validate the recomputed forest.
132        forest.validate()?;
133
134        Ok(forest)
135    }
136
137    /// Deserializes an [`UntrustedMastForest`] from bytes.
138    ///
139    /// This method uses a [`BudgetedReader`] plus a bounded validation-allocation budget derived
140    /// from the input size to protect against denial-of-service attacks from malicious input.
141    /// The default validation budget includes room for the retained serialized copy used by the
142    /// deferred-validation path, in addition to hashless helper allocations. Concretely,
143    /// the default is `bytes.len()` for parsing and `bytes.len() * 7` for validation allocations.
144    /// That `* 7` factor is a coarse convenience bound, not an exact peak-memory formula.
145    ///
146    /// For an explicit wire parsing limit, use
147    /// [`read_from_bytes_with_options`](Self::read_from_bytes_with_options).
148    ///
149    /// # Example
150    ///
151    /// ```ignore
152    /// // Read from untrusted source
153    /// let untrusted = UntrustedMastForest::read_from_bytes(&bytes)?;
154    ///
155    /// // Validate before use
156    /// let forest = untrusted.validate()?;
157    /// ```
158    #[track_caller]
159    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
160        Self::read_from_bytes_with_options(bytes, UntrustedMastForestReadOptions::default())
161    }
162
163    /// Deserializes an [`UntrustedMastForest`] from bytes with explicit options.
164    ///
165    /// The wire byte budget limits wire-driven parsing and collection pre-sizing. The validation
166    /// helper-allocation budget is derived from that wire budget and caps tracked hashless helper
167    /// allocations such as digest slot tables and rebuilt digest tables.
168    #[track_caller]
169    pub fn read_from_bytes_with_options(
170        bytes: &[u8],
171        options: UntrustedMastForestReadOptions,
172    ) -> Result<Self, DeserializationError> {
173        let caller = Location::caller();
174        let wire_byte_budget = options.wire_byte_budget(bytes.len());
175        let mut reader = BudgetedReader::new(SliceReader::new(bytes), wire_byte_budget);
176        let (forest, _flags) =
177            serialization::read_untrusted_with_flags_allocation_budget_and_caller(
178                &mut reader,
179                options.validation_allocation_budget(wire_byte_budget),
180                caller,
181            )?;
182        if reader.has_more_bytes() {
183            return Err(DeserializationError::InvalidValue(
184                "extra bytes after MastForest payload".into(),
185            ));
186        }
187        Ok(forest)
188    }
189}