Skip to main content

miden_core/deferred/
wire.rs

1//! Portable singleton deferred witnesses and their versioned encoding.
2//!
3//! Index zero is implicit TRUE. Canonical witnesses reference earlier entries and end with the
4//! selected root, but decoding establishes only bounded transport syntax. Preparation validates
5//! the graph, computes commitments, accounts its declared work, and establishes the root before
6//! native or STARK-backed evaluation.
7
8use alloc::{
9    collections::{BTreeMap, BTreeSet},
10    format,
11    sync::Arc,
12    vec::Vec,
13};
14
15use miden_crypto::hash::eidos::{EidosDomain, EidosFrame};
16
17use super::{
18    DEFERRED_AND_FRAME, DataChunk, DeferredError, DeferredState, Digest,
19    MAX_DEFERRED_WIRE_ELEMENTS, Node, NodeType, PrecompileError, PrecompileLimits,
20    PrecompileRegistry, PrecompileWork, TRUE_DIGEST,
21};
22use crate::{
23    Felt, Word, ZERO,
24    program::domain::DeferredChunksDomain,
25    serde::{
26        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
27        SliceReader, validate_bounded_len,
28    },
29};
30
31// CONSTANTS
32// ================================================================================================
33
34/// Reserved index for the always-known [`super::TRUE_DIGEST`] / [`super::Node::TRUE`] node.
35const TRUE_INDEX: u32 = 0;
36
37const MAX_WIRE_ENTRIES: usize = MAX_DEFERRED_WIRE_ELEMENTS / EidosFrame::FELT_LEN;
38
39fn reserve_wire_elements(
40    remaining_elements: &mut usize,
41    requested_elements: usize,
42) -> Result<(), DeserializationError> {
43    *remaining_elements = remaining_elements.checked_sub(requested_elements).ok_or_else(|| {
44        DeserializationError::InvalidValue(format!(
45            "deferred wire exceeds the {MAX_DEFERRED_WIRE_ELEMENTS} element limit"
46        ))
47    })?;
48    Ok(())
49}
50
51fn reserve_wire_payload(
52    remaining_elements: &mut usize,
53    payload_count: usize,
54) -> Result<(), DeserializationError> {
55    let payload_elements =
56        payload_count.checked_mul(Node::DATA_CHUNK_FELT_LEN).ok_or_else(|| {
57            DeserializationError::InvalidValue("deferred wire element count overflow".into())
58        })?;
59    reserve_wire_elements(remaining_elements, payload_elements)
60}
61
62// WIRE ENTRY
63// ================================================================================================
64
65/// One untrusted deferred DAG wire entry.
66///
67/// Wire index 0 is implicit TRUE. `entries[i]` has wire index `i + 1`. Preparation requires
68/// structural children to reference `TRUE_INDEX` or an earlier entry. Pair-list pairs store
69/// structural child references in payload order.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum WireEntry {
72    /// Raw data payload interpreted by the frame's precompile.
73    ///
74    /// The payload requires at least one chunk. A frame's precompile may assign value semantics to
75    /// a one-chunk payload, but the wire shape itself does not.
76    Data {
77        frame: EidosFrame,
78        chunks: Vec<DataChunk>,
79    },
80    /// Two child references resolved against `TRUE_INDEX` or earlier wire indices.
81    Join { frame: EidosFrame, lhs: u32, rhs: u32 },
82    /// Raw structural child-reference pairs, with at least one pair.
83    PairList {
84        frame: EidosFrame,
85        pairs: Vec<(u32, u32)>,
86    },
87}
88
89impl WireEntry {
90    // An empty Data/PairList is transport-valid but rejected during preparation.
91    fn min_serialized_size() -> usize {
92        1 + Word::min_serialized_size() + usize::min_serialized_size()
93    }
94}
95
96// PORTABLE WITNESS
97// ================================================================================================
98
99/// Bounded, untrusted portable entries for one deferred execution obligation.
100///
101/// Decoding establishes only canonical transport syntax and allocation bounds. Use
102/// [`Self::prepare`] to validate the graph, compute its commitments and root, and enforce declared
103/// work limits before evaluation.
104#[derive(Debug, Clone, PartialEq, Eq)]
105#[cfg_attr(
106    all(feature = "arbitrary", test),
107    miden_test_serialization_macros::serialization_test
108)]
109pub struct PrecompileWitness {
110    entries: Vec<WireEntry>,
111}
112
113impl PrecompileWitness {
114    /// Version of the standalone singleton witness encoding. Other versions are rejected.
115    pub const WIRE_VERSION: u8 = 2;
116
117    /// Creates bounded portable entries without hashing or validating their graph.
118    pub fn from_entries(entries: Vec<WireEntry>) -> Result<Self, IntegrityError> {
119        let wire = Self { entries };
120        wire.validate_element_limit()?;
121        Ok(wire)
122    }
123
124    /// Consumes and admits this graph under `registry` and `verification_limits`, computing each
125    /// commitment once without evaluating precompile semantics or establishing assertion truth.
126    /// Callers retaining the portable witness must clone it explicitly.
127    pub fn prepare(
128        self,
129        registry: Arc<PrecompileRegistry>,
130        verification_limits: &PrecompileLimits,
131    ) -> Result<PreparedWitness, PreparationError> {
132        if self.entries.is_empty() {
133            return Err(IntegrityError::InvalidStructure.into());
134        }
135        let mut indices = BTreeMap::new();
136        let mut nodes: Vec<PreparedNode> = Vec::with_capacity(self.entries.len());
137        let mut work = PrecompileWork::default();
138        indices.insert(TRUE_DIGEST, 0);
139
140        for entry in self.entries {
141            let child = |index: u32| match index {
142                TRUE_INDEX => Ok(TRUE_DIGEST),
143                index => nodes
144                    .get(index as usize - 1)
145                    .map(PreparedNode::digest)
146                    .ok_or(IntegrityError::InvalidStructure),
147            };
148            let node = match entry {
149                WireEntry::Data { frame, chunks } => {
150                    if frame.domain() == DeferredChunksDomain::TAG {
151                        if Node::chunks_frame_for_len(chunks.len())
152                            .map_err(|_| IntegrityError::InvalidStructure)?
153                            != frame
154                        {
155                            return Err(IntegrityError::InvalidStructure.into());
156                        }
157                        Node::chunks(chunks)
158                    } else {
159                        Node::try_data(frame, chunks)
160                    }
161                },
162                WireEntry::Join { frame, lhs, rhs } => {
163                    let (lhs, rhs) = (child(lhs)?, child(rhs)?);
164                    if frame == DEFERRED_AND_FRAME {
165                        Ok(Node::and(lhs, rhs))
166                    } else {
167                        Node::join(frame, lhs, rhs)
168                    }
169                },
170                WireEntry::PairList { frame, pairs } => {
171                    let pairs = pairs
172                        .into_iter()
173                        .map(|(lhs, rhs)| Ok((child(lhs)?, child(rhs)?)))
174                        .collect::<Result<Vec<_>, IntegrityError>>()?;
175                    Node::try_pair_list(frame, pairs)
176                },
177            }
178            .map_err(|_| IntegrityError::InvalidStructure)?;
179
180            registry.validate_node(&node)?;
181            work.charge_node(&node, &registry, verification_limits)?;
182            let prepared = PreparedNode::new(node);
183            let digest = prepared.digest();
184            if indices.insert(digest, nodes.len() + 1).is_some() {
185                return Err(IntegrityError::InvalidStructure.into());
186            }
187            nodes.push(prepared);
188        }
189
190        Self::validate_canonical_order(&nodes, &indices)?;
191        let root = nodes.last().expect("the witness is nonempty").digest();
192        if root == TRUE_DIGEST {
193            return Err(IntegrityError::InvalidStructure.into());
194        }
195
196        Ok(PreparedWitness { registry, nodes, root, work })
197    }
198
199    /// Returns the bounded, untrusted wire entries. Index zero denotes implicit TRUE.
200    pub fn entries(&self) -> &[WireEntry] {
201        &self.entries
202    }
203
204    fn validate_canonical_order(
205        nodes: &[PreparedNode],
206        indices: &BTreeMap<Digest, usize>,
207    ) -> Result<(), IntegrityError> {
208        // The same left-to-right DFS used by the exporter must emit exactly the supplied stream.
209        // Backward references make this iterative traversal acyclic, including for shared graphs.
210        let mut seen = alloc::vec![false; nodes.len() + 1];
211        let mut pending = alloc::vec![(nodes.len(), false)];
212        let mut next_index = 1;
213        while let Some((index, emit)) = pending.pop() {
214            if index == 0 {
215                continue;
216            }
217            if emit {
218                if index != next_index {
219                    return Err(IntegrityError::InvalidStructure);
220                }
221                next_index += 1;
222            } else if !core::mem::replace(&mut seen[index], true) {
223                pending.push((index, true));
224                pending.extend(
225                    nodes[index - 1].node().children().rev().map(|child| (indices[&child], false)),
226                );
227            }
228        }
229        if next_index != nodes.len() + 1 {
230            return Err(IntegrityError::InvalidStructure);
231        }
232        Ok(())
233    }
234
235    /// Exports and admits the graph reachable from `root` without serializing through bytes.
236    pub(crate) fn from_state(
237        state: &DeferredState,
238        root: Digest,
239        verification_limits: &PrecompileLimits,
240    ) -> Result<Self, PrecompileError> {
241        if root == TRUE_DIGEST {
242            return Err(PrecompileError::InvalidNode);
243        }
244        let mut build = WireEncoder::default();
245        build.visit_state_digest(state, root, verification_limits)?;
246        Ok(Self { entries: build.entries })
247    }
248
249    fn validate_element_limit(&self) -> Result<(), IntegrityError> {
250        let mut remaining_elements = MAX_DEFERRED_WIRE_ELEMENTS;
251        for entry in &self.entries {
252            let payload_count = match entry {
253                WireEntry::Data { chunks, .. } => chunks.len(),
254                WireEntry::Join { .. } => 1,
255                WireEntry::PairList { pairs, .. } => pairs.len(),
256            };
257            let payload_elements = payload_count
258                .checked_mul(Node::DATA_CHUNK_FELT_LEN)
259                .and_then(|elements| EidosFrame::FELT_LEN.checked_add(elements))
260                .ok_or(IntegrityError::InvalidStructure)?;
261            remaining_elements = remaining_elements.checked_sub(payload_elements).ok_or(
262                IntegrityError::DeferredStateTooLarge {
263                    num_elements: payload_elements,
264                    max: remaining_elements,
265                },
266            )?;
267        }
268        Ok(())
269    }
270}
271
272// PREPARED WITNESS
273// ================================================================================================
274
275/// A hydrated node and the commitment computed during preparation.
276///
277/// Obtained from [`PreparedWitness::into_nodes`]. Fields and construction remain private so
278/// consumers can inspect the checked definition without changing it or its commitment.
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct PreparedNode {
281    node: Node,
282    digest: Digest,
283}
284
285impl PreparedNode {
286    pub(super) fn new(node: Node) -> Self {
287        let digest = node.digest();
288        Self { node, digest }
289    }
290
291    pub(super) fn into_parts(self) -> (Digest, Node) {
292        (self.digest, self.node)
293    }
294
295    /// Returns the hydrated node.
296    pub const fn node(&self) -> &Node {
297        &self.node
298    }
299
300    /// Returns the node's checked commitment.
301    pub const fn digest(&self) -> Digest {
302        self.digest
303    }
304}
305
306/// A structurally valid, admitted witness ready for native or recording evaluation.
307///
308/// Preparation establishes graph integrity, commitments, and declared work only. It does not
309/// establish semantic validity or assertion truth.
310#[derive(Debug, Clone)]
311pub struct PreparedWitness {
312    registry: Arc<PrecompileRegistry>,
313    nodes: Vec<PreparedNode>,
314    root: Digest,
315    work: PrecompileWork,
316}
317
318impl PreparedWitness {
319    /// Returns the root computed from the prepared graph.
320    pub const fn root(&self) -> Digest {
321        self.root
322    }
323
324    /// Returns the admitted verification workload summary.
325    pub const fn work(&self) -> &PrecompileWork {
326        &self.work
327    }
328
329    /// Consumes this witness and yields its checked nodes in canonical child-first order.
330    ///
331    /// The nodes establish structural validity and admission, not computational validity.
332    pub fn into_nodes(self) -> impl ExactSizeIterator<Item = PreparedNode> {
333        self.nodes.into_iter()
334    }
335
336    /// Consumes and evaluates this witness, succeeding only if its root resolves to
337    /// [`TRUE_DIGEST`].
338    pub fn evaluate(self) -> Result<(), PrecompileError> {
339        let Self { registry, nodes, root: _, work: _ } = self;
340        let mut evaluator = super::state::DeferredEvaluator::new(registry)?;
341        let mut digests = Vec::with_capacity(nodes.len());
342        for prepared in nodes {
343            digests.push(evaluator.insert_node(prepared)?);
344        }
345        let root_digest = digests.pop().ok_or(PrecompileError::MissingNode)?;
346        for digest in digests {
347            evaluator.evaluate_digest(digest)?;
348        }
349        if evaluator.evaluate_digest(root_digest)? != TRUE_DIGEST {
350            return Err(PrecompileError::AssertionFailed);
351        }
352        Ok(())
353    }
354}
355
356/// Failure while turning bounded wire material into a prepared witness.
357#[derive(Debug, Clone, thiserror::Error)]
358pub enum PreparationError {
359    #[error(transparent)]
360    Integrity(#[from] IntegrityError),
361    #[error(transparent)]
362    Precompile(#[from] PrecompileError),
363}
364
365// INTEGRITY ERROR
366// ================================================================================================
367
368/// A portable graph cannot be represented within the structural and resource constraints.
369#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
370pub enum IntegrityError {
371    /// Invalid framework shape, child reference, duplicate, orphan, or canonical entry order.
372    #[error("invalid or non-canonical precompile witness structure")]
373    InvalidStructure,
374    /// The portable entries exceed the hard wire-allocation ceiling.
375    #[error("deferred insertion requires {num_elements} elements but only {max} remain")]
376    DeferredStateTooLarge { num_elements: usize, max: usize },
377}
378
379// WIRE ENCODING
380// ================================================================================================
381
382/// Iterative exporter for the original, root-reachable execution graph.
383#[derive(Default)]
384struct WireEncoder {
385    seen: BTreeSet<Digest>,
386    by_digest: BTreeMap<Digest, u32>,
387    entries: Vec<WireEntry>,
388}
389
390impl WireEncoder {
391    fn visit_state_digest(
392        &mut self,
393        state: &DeferredState,
394        digest: Digest,
395        verification_limits: &PrecompileLimits,
396    ) -> Result<(), PrecompileError> {
397        let mut pending = Vec::new();
398        let mut work = PrecompileWork::default();
399        pending.push(WireEncodeStep::Visit(digest));
400
401        while let Some(step) = pending.pop() {
402            match step {
403                WireEncodeStep::Visit(digest) => self.schedule_digest(
404                    state,
405                    digest,
406                    &mut pending,
407                    &mut work,
408                    verification_limits,
409                )?,
410                WireEncodeStep::Emit(digest, node_type) => {
411                    let entry = self.entry_for_digest(state, digest, node_type)?;
412                    self.push_entry(digest, entry)?;
413                },
414            }
415        }
416
417        Ok(())
418    }
419
420    fn schedule_digest(
421        &mut self,
422        state: &DeferredState,
423        digest: Digest,
424        pending: &mut Vec<WireEncodeStep>,
425        work: &mut PrecompileWork,
426        verification_limits: &PrecompileLimits,
427    ) -> Result<(), PrecompileError> {
428        if digest == TRUE_DIGEST || self.seen.contains(&digest) {
429            return Ok(());
430        }
431
432        let (node_type, node) = self.validated_node(state, digest)?;
433        work.charge_node(node, state.registry(), verification_limits)?;
434        if work.elements() > MAX_DEFERRED_WIRE_ELEMENTS as u64 {
435            return Err(DeferredError::DeferredStateTooLarge {
436                num_elements: usize::try_from(work.elements()).unwrap_or(usize::MAX),
437                max: MAX_DEFERRED_WIRE_ELEMENTS,
438            }
439            .into());
440        }
441        self.seen.insert(digest);
442        pending.push(WireEncodeStep::Emit(digest, node_type));
443
444        match node_type {
445            NodeType::Data => {},
446            NodeType::Join => {
447                let (lhs, rhs) = node.payload().as_join()?;
448                pending.push(WireEncodeStep::Visit(rhs));
449                pending.push(WireEncodeStep::Visit(lhs));
450            },
451            NodeType::PairList => {
452                let pairs = node.payload().as_pair_list()?;
453                for (lhs, rhs) in pairs.iter().rev() {
454                    pending.push(WireEncodeStep::Visit(*rhs));
455                    pending.push(WireEncodeStep::Visit(*lhs));
456                }
457            },
458            NodeType::True => return Err(PrecompileError::InvalidNode),
459        };
460
461        Ok(())
462    }
463
464    fn entry_for_digest(
465        &self,
466        state: &DeferredState,
467        digest: Digest,
468        node_type: NodeType,
469    ) -> Result<WireEntry, PrecompileError> {
470        let node = state.get_node(&digest).ok_or(PrecompileError::MissingNode)?;
471
472        Ok(match node_type {
473            NodeType::Data => WireEntry::Data {
474                frame: node.frame().ok_or(PrecompileError::InvalidNode)?,
475                chunks: node.payload().as_data()?.to_vec(),
476            },
477            NodeType::Join => {
478                let (lhs, rhs) = node.payload().as_join()?;
479                let lhs = self.index_for(lhs)?;
480                let rhs = self.index_for(rhs)?;
481                WireEntry::Join {
482                    frame: node.frame().ok_or(PrecompileError::InvalidNode)?,
483                    lhs,
484                    rhs,
485                }
486            },
487            NodeType::PairList => {
488                let pairs = node.payload().as_pair_list()?;
489                let pairs = pairs
490                    .iter()
491                    .map(|(lhs, rhs)| Ok((self.index_for(*lhs)?, self.index_for(*rhs)?)))
492                    .collect::<Result<Vec<_>, PrecompileError>>()?;
493                WireEntry::PairList {
494                    frame: node.frame().ok_or(PrecompileError::InvalidNode)?,
495                    pairs,
496                }
497            },
498            NodeType::True => return Err(PrecompileError::InvalidNode),
499        })
500    }
501
502    fn validated_node<'a>(
503        &self,
504        state: &'a DeferredState,
505        digest: Digest,
506    ) -> Result<(NodeType, &'a Node), PrecompileError> {
507        let node = state.get_node(&digest).ok_or(PrecompileError::MissingNode)?;
508        let node_type = state.registry().validate_node(node)?;
509        Ok((node_type, node))
510    }
511
512    fn index_for(&self, digest: Digest) -> Result<u32, PrecompileError> {
513        if digest == TRUE_DIGEST {
514            return Ok(TRUE_INDEX);
515        }
516        self.by_digest.get(&digest).copied().ok_or(PrecompileError::MissingNode)
517    }
518
519    fn push_entry(&mut self, digest: Digest, entry: WireEntry) -> Result<(), PrecompileError> {
520        let next_index = self.entries.len().checked_add(1).ok_or(PrecompileError::InvalidNode)?;
521        let next_index = u32::try_from(next_index).map_err(|_| PrecompileError::InvalidNode)?;
522        self.entries.push(entry);
523        self.by_digest.insert(digest, next_index);
524        Ok(())
525    }
526}
527
528enum WireEncodeStep {
529    Visit(Digest),
530    Emit(Digest, NodeType),
531}
532
533// SERIALIZATION
534// ================================================================================================
535
536impl Serializable for WireEntry {
537    fn write_into<W: ByteWriter>(&self, target: &mut W) {
538        match self {
539            Self::Data { frame, chunks } => {
540                target.write_u8(0);
541                frame.as_word().write_into(target);
542                target.write_usize(chunks.len());
543                for chunk in chunks {
544                    for felt in chunk {
545                        felt.write_into(target);
546                    }
547                }
548            },
549            Self::Join { frame, lhs, rhs } => {
550                target.write_u8(1);
551                frame.as_word().write_into(target);
552                target.write_u32(*lhs);
553                target.write_u32(*rhs);
554            },
555            Self::PairList { frame, pairs } => {
556                target.write_u8(2);
557                frame.as_word().write_into(target);
558                target.write_usize(pairs.len());
559                for (lhs, rhs) in pairs {
560                    target.write_u32(*lhs);
561                    target.write_u32(*rhs);
562                }
563            },
564        }
565    }
566}
567
568struct WirePair((u32, u32));
569
570impl Deserializable for WirePair {
571    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
572        Ok(Self((source.read_u32()?, source.read_u32()?)))
573    }
574
575    fn min_serialized_size() -> usize {
576        u32::min_serialized_size() * 2
577    }
578}
579
580struct WireDataChunk(DataChunk);
581
582impl Deserializable for WireDataChunk {
583    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
584        let mut chunk = [ZERO; Node::DATA_CHUNK_FELT_LEN];
585        for felt in &mut chunk {
586            *felt = Felt::read_from(source)?;
587        }
588        Ok(Self(chunk))
589    }
590
591    fn min_serialized_size() -> usize {
592        Node::DATA_CHUNK_FELT_LEN * Felt::min_serialized_size()
593    }
594}
595
596impl Serializable for PrecompileWitness {
597    fn write_into<W: ByteWriter>(&self, target: &mut W) {
598        target.write_u8(Self::WIRE_VERSION);
599        target.write_usize(self.entries.len());
600        for entry in &self.entries {
601            entry.write_into(target);
602        }
603    }
604}
605
606impl Deserializable for PrecompileWitness {
607    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
608        let version = source.read_u8()?;
609        if version != Self::WIRE_VERSION {
610            return Err(DeserializationError::InvalidValue(format!(
611                "unsupported precompile witness version {version} (expected {})",
612                Self::WIRE_VERSION
613            )));
614        }
615        let entry_count =
616            read_len(source, "precompile witness entry", WireEntry::min_serialized_size())?;
617        if entry_count > MAX_WIRE_ENTRIES {
618            return Err(DeserializationError::InvalidValue(format!(
619                "precompile witness contains {entry_count} entries, maximum is {MAX_WIRE_ENTRIES}"
620            )));
621        }
622
623        let mut remaining_elements = MAX_DEFERRED_WIRE_ELEMENTS;
624        let mut entries = Vec::with_capacity(entry_count);
625        for _ in 0..entry_count {
626            entries.push(read_wire_entry(source, &mut remaining_elements)?);
627        }
628        Ok(Self { entries })
629    }
630
631    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
632        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
633        let wire = Self::read_from(&mut reader)?;
634        if reader.has_more_bytes() {
635            return Err(DeserializationError::InvalidValue(
636                "trailing bytes after deferred witness".into(),
637            ));
638        }
639        Ok(wire)
640    }
641
642    fn min_serialized_size() -> usize {
643        1 + usize::min_serialized_size()
644    }
645}
646
647/// Retain the existing vint64 decoder and allocation checks, requiring its shortest encoding.
648fn read_len<R: ByteReader>(
649    source: &mut R,
650    label: &str,
651    min_element_size: usize,
652) -> Result<usize, DeserializationError> {
653    let encoded_len = source.peek_u8()?.trailing_zeros() as usize + 1;
654    let len = source.read_usize()?;
655    // The usize Serializable implementation computes the exact vint64 width without allocating.
656    if encoded_len != len.get_size_hint() {
657        return Err(DeserializationError::InvalidValue(format!("noncanonical {label} length")));
658    }
659    validate_bounded_len(source, label, len, min_element_size)?;
660    Ok(len)
661}
662
663fn read_wire_entry<R: ByteReader>(
664    source: &mut R,
665    remaining_elements: &mut usize,
666) -> Result<WireEntry, DeserializationError> {
667    let discriminant = source.read_u8()?;
668    match discriminant {
669        0 => {
670            reserve_wire_elements(remaining_elements, EidosFrame::FELT_LEN)?;
671            let frame = read_frame(source)?;
672            let chunk_count = read_len(source, "data chunk", WireDataChunk::min_serialized_size())?;
673            reserve_wire_payload(remaining_elements, chunk_count)?;
674            let chunks = source
675                .read_many_iter::<WireDataChunk>(chunk_count)?
676                .map(|chunk| chunk.map(|chunk| chunk.0))
677                .collect::<Result<_, _>>()?;
678            Ok(WireEntry::Data { frame, chunks })
679        },
680        1 => {
681            reserve_wire_elements(
682                remaining_elements,
683                EidosFrame::FELT_LEN + Node::DATA_CHUNK_FELT_LEN,
684            )?;
685            let frame = read_frame(source)?;
686            let lhs = source.read_u32()?;
687            let rhs = source.read_u32()?;
688            Ok(WireEntry::Join { frame, lhs, rhs })
689        },
690        2 => {
691            reserve_wire_elements(remaining_elements, EidosFrame::FELT_LEN)?;
692            let frame = read_frame(source)?;
693            let pair_count = read_len(source, "child pair", WirePair::min_serialized_size())?;
694            reserve_wire_payload(remaining_elements, pair_count)?;
695            let pairs = source
696                .read_many_iter::<WirePair>(pair_count)?
697                .map(|pair| pair.map(|pair| pair.0))
698                .collect::<Result<_, _>>()?;
699            Ok(WireEntry::PairList { frame, pairs })
700        },
701        other => Err(DeserializationError::InvalidValue(format!(
702            "invalid deferred wire entry discriminant: {other}"
703        ))),
704    }
705}
706
707fn read_frame<R: ByteReader>(source: &mut R) -> Result<EidosFrame, DeserializationError> {
708    EidosFrame::from_word(Word::read_from(source)?)
709        .ok_or_else(|| DeserializationError::InvalidValue("invalid deferred Eidos frame".into()))
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715    use crate::deferred::{
716        PrecompileLimitError, deferred_chunks_frame, precompile::test_precompile_domain_tag,
717    };
718
719    fn felts(seed: u64) -> DataChunk {
720        core::array::from_fn(|i| Felt::new_unchecked(seed + i as u64))
721    }
722
723    fn frame(seed: u8) -> EidosFrame {
724        EidosFrame::new(test_precompile_domain_tag(seed), [u32::from(seed), 0, 0])
725    }
726
727    fn encoded_entries(entries: &[WireEntry]) -> Vec<u8> {
728        let mut bytes = alloc::vec![PrecompileWitness::WIRE_VERSION];
729        bytes.write_usize(entries.len());
730        for entry in entries {
731            entry.write_into(&mut bytes);
732        }
733        bytes
734    }
735
736    fn framework_limits() -> PrecompileLimits {
737        PrecompileLimits::new(MAX_DEFERRED_WIRE_ELEMENTS as u64)
738    }
739
740    fn prepare_framework(witness: PrecompileWitness) -> Result<PreparedWitness, PreparationError> {
741        witness.prepare(Arc::new(PrecompileRegistry::new()), &framework_limits())
742    }
743
744    #[test]
745    fn portable_structure_is_independent_of_operation_support() {
746        let entries = alloc::vec![
747            WireEntry::Data {
748                frame: frame(1),
749                chunks: alloc::vec![felts(10)]
750            },
751            WireEntry::Data {
752                frame: frame(2),
753                chunks: alloc::vec![felts(20), felts(30)]
754            },
755            WireEntry::Join { frame: frame(3), lhs: 1, rhs: 1 },
756            WireEntry::PairList {
757                frame: frame(4),
758                pairs: alloc::vec![(1, 2), (3, 3)]
759            },
760        ];
761        let witness = PrecompileWitness::from_entries(entries).unwrap();
762        let left = Node::value(frame(1), felts(10)).unwrap().digest();
763        let right = Node::try_data(frame(2), alloc::vec![felts(20), felts(30)]).unwrap().digest();
764        let claim = Node::join(frame(3), left, left).unwrap().digest();
765        let expected = Node::try_pair_list(frame(4), alloc::vec![(left, right), (claim, claim)])
766            .unwrap()
767            .digest();
768        assert_ne!(expected, TRUE_DIGEST);
769        assert_eq!(PrecompileWitness::read_from_bytes(&witness.to_bytes()).unwrap(), witness);
770        assert!(matches!(
771            prepare_framework(witness),
772            Err(PreparationError::Precompile(PrecompileError::InvalidNode))
773        ));
774    }
775
776    #[test]
777    fn portable_decode_defers_graph_validation_until_preparation() {
778        let leaf = || WireEntry::Data {
779            frame: deferred_chunks_frame(1),
780            chunks: alloc::vec![felts(10)],
781        };
782        let other = || WireEntry::Data {
783            frame: deferred_chunks_frame(1),
784            chunks: alloc::vec![felts(20)],
785        };
786        let malformed_and = EidosFrame::new(DEFERRED_AND_FRAME.domain(), [1, 0, 0]);
787        let graph_invalid = [
788            Vec::new(),
789            alloc::vec![WireEntry::Data {
790                frame: deferred_chunks_frame(1),
791                chunks: Vec::new()
792            }],
793            alloc::vec![WireEntry::PairList { frame: frame(1), pairs: Vec::new() }],
794            alloc::vec![WireEntry::Data {
795                frame: DEFERRED_AND_FRAME,
796                chunks: alloc::vec![felts(10)]
797            }],
798            alloc::vec![WireEntry::Data {
799                frame: deferred_chunks_frame(2),
800                chunks: alloc::vec![felts(10)]
801            }],
802            alloc::vec![WireEntry::Join {
803                frame: deferred_chunks_frame(1),
804                lhs: 0,
805                rhs: 0
806            }],
807            alloc::vec![WireEntry::Join { frame: malformed_and, lhs: 0, rhs: 0 }],
808            alloc::vec![WireEntry::Join {
809                frame: DEFERRED_AND_FRAME,
810                lhs: 0,
811                rhs: 1
812            }],
813            alloc::vec![
814                leaf(),
815                leaf(),
816                WireEntry::Join {
817                    frame: DEFERRED_AND_FRAME,
818                    lhs: 1,
819                    rhs: 2
820                }
821            ],
822            alloc::vec![leaf(), other()],
823            alloc::vec![
824                leaf(),
825                other(),
826                WireEntry::Join {
827                    frame: DEFERRED_AND_FRAME,
828                    lhs: 2,
829                    rhs: 1
830                }
831            ],
832            alloc::vec![
833                WireEntry::Join {
834                    frame: DEFERRED_AND_FRAME,
835                    lhs: 0,
836                    rhs: 2
837                },
838                WireEntry::Join {
839                    frame: DEFERRED_AND_FRAME,
840                    lhs: 0,
841                    rhs: 0
842                },
843            ],
844            alloc::vec![WireEntry::PairList {
845                frame: frame(1),
846                pairs: alloc::vec![(0, 1)]
847            }],
848        ];
849        for entries in graph_invalid {
850            let bytes = encoded_entries(&entries);
851            let witness = PrecompileWitness::from_entries(entries).unwrap();
852            assert_eq!(PrecompileWitness::read_from_bytes(&bytes).unwrap(), witness);
853            assert!(matches!(
854                prepare_framework(witness),
855                Err(PreparationError::Integrity(IntegrityError::InvalidStructure))
856            ));
857        }
858    }
859
860    #[test]
861    fn export_omits_unreachable_state_and_retains_logged_true() {
862        let mut empty = DeferredState::default();
863        let value = empty.register(Node::chunks(alloc::vec![felts(10)]).unwrap()).unwrap();
864        // Borrowed export is structural and works even when the selected node is not a statement.
865        let witness = empty.witness_for(value, &framework_limits()).unwrap();
866        assert_eq!(
867            witness
868                .prepare(Arc::new(PrecompileRegistry::new()), &framework_limits())
869                .unwrap()
870                .root(),
871            value
872        );
873        assert!(matches!(
874            empty.witness_for(value, &PrecompileLimits::new(0)),
875            Err(PrecompileError::Limit(PrecompileLimitError::Elements { max: 0, .. }))
876        ));
877        assert_eq!(empty.root(), TRUE_DIGEST);
878        assert!(empty.get_node(&value).is_some());
879        for root in [TRUE_DIGEST, Word::new([Felt::from_u32(99); 4])] {
880            assert!(empty.witness_for(root, &framework_limits()).is_err());
881        }
882        assert!(empty.into_witness(&framework_limits()).unwrap().is_none());
883        let mut state = DeferredState::default();
884        state.register(Node::chunks(alloc::vec![felts(10)]).unwrap()).unwrap();
885        state.log_statement(TRUE_DIGEST).unwrap();
886        let root = state.root();
887        let expected_elements = Node::and(TRUE_DIGEST, TRUE_DIGEST).felt_len() as u64;
888        let verification_limits = PrecompileLimits::new(expected_elements);
889        let registry = Arc::new(PrecompileRegistry::new());
890        let witness = state.clone().into_witness(&verification_limits).unwrap().unwrap();
891        assert_eq!(
892            witness.entries(),
893            &[WireEntry::Join {
894                frame: DEFERRED_AND_FRAME,
895                lhs: 0,
896                rhs: 0
897            }]
898        );
899        let prepared = witness
900            .prepare(registry, &verification_limits)
901            .expect("exported work must prepare under the same verification policy");
902        assert_eq!(prepared.root(), root);
903        assert_eq!(prepared.work().nodes(), 1);
904        assert_eq!(prepared.work().elements(), expected_elements);
905        prepared.evaluate().expect("prepared evaluation is semantic only");
906
907        let too_small = verification_limits.with_max_elements(expected_elements - 1);
908        assert!(matches!(
909            state.into_witness(&too_small),
910            Err(PrecompileError::Limit(PrecompileLimitError::Elements {
911                actual,
912                max,
913            })) if actual == expected_elements && max == expected_elements - 1
914        ));
915    }
916
917    #[test]
918    fn deep_shared_graph_exports_and_decodes_without_expansion() {
919        let mut state = DeferredState::default();
920        let mut statement = TRUE_DIGEST;
921        for _ in 0..4_096 {
922            statement = state.register(Node::and(statement, statement)).unwrap();
923        }
924        state.log_statement(statement).unwrap();
925        let root = state.root();
926        let witness = state.into_witness(&framework_limits()).unwrap().unwrap();
927        assert_eq!(witness.entries().len(), 4_097);
928        assert_eq!(PrecompileWitness::from_entries(witness.entries().to_vec()).unwrap(), witness);
929        assert_eq!(PrecompileWitness::read_from_bytes(&witness.to_bytes()).unwrap(), witness);
930        assert_eq!(prepare_framework(witness).unwrap().root(), root);
931    }
932
933    #[test]
934    fn standalone_witness_rejects_legacy_tag_encoding_before_body() {
935        // Version-1 singleton Join with Tag::AND = [1, 0, 0, 0] and two TRUE children.
936        const LEGACY_WITNESS: &[u8] = &[
937            1, 3, 1, // Version, one entry (vint64), Join discriminant.
938            1, 0, 0, 0, 0, 0, 0, 0, // Tag id.
939            0, 0, 0, 0, 0, 0, 0, 0, // Tag arg0.
940            0, 0, 0, 0, 0, 0, 0, 0, // Tag arg1.
941            0, 0, 0, 0, 0, 0, 0, 0, // Tag arg2.
942            0, 0, 0, 0, 0, 0, 0, 0, // Two u32 child indices.
943        ];
944        let mut reader = SliceReader::new(LEGACY_WITNESS);
945        assert_eq!(
946            PrecompileWitness::read_from(&mut reader),
947            Err(DeserializationError::InvalidValue(
948                "unsupported precompile witness version 1 (expected 2)".into()
949            ))
950        );
951        assert_eq!(reader.read_slice(LEGACY_WITNESS.len() - 1).unwrap(), &LEGACY_WITNESS[1..]);
952    }
953
954    #[test]
955    fn standalone_witness_rejects_unsupported_versions_and_trailing_bytes() {
956        let witness = PrecompileWitness::from_entries(alloc::vec![WireEntry::Join {
957            frame: DEFERRED_AND_FRAME,
958            lhs: 0,
959            rhs: 0
960        }])
961        .unwrap();
962        let bytes = witness.to_bytes();
963        assert_eq!(bytes[0], 2);
964        for version in [0, 1, PrecompileWitness::WIRE_VERSION + 1] {
965            let mut unsupported = bytes.clone();
966            unsupported[0] = version;
967            assert_eq!(
968                PrecompileWitness::read_from_bytes(&unsupported),
969                Err(DeserializationError::InvalidValue(format!(
970                    "unsupported precompile witness version {version} (expected 2)"
971                )))
972            );
973        }
974        let mut trailing = bytes;
975        trailing.push(0);
976        assert!(PrecompileWitness::read_from_bytes(&trailing).is_err());
977    }
978
979    #[test]
980    fn decoder_rejects_malformed_frame_words_for_every_entry_shape() {
981        let entries = [
982            WireEntry::Data {
983                frame: frame(1),
984                chunks: alloc::vec![felts(10)],
985            },
986            WireEntry::Join { frame: frame(1), lhs: 0, rhs: 0 },
987            WireEntry::PairList {
988                frame: frame(1),
989                pairs: alloc::vec![(0, 0)],
990            },
991        ];
992        let mut invalid_frames = alloc::vec![Word::default()];
993        for index in 0..EidosFrame::FELT_LEN {
994            let mut elements = frame(1).as_word().into_elements();
995            elements[index] = Felt::new_unchecked(u32::MAX as u64 + 1);
996            invalid_frames.push(Word::new(elements));
997        }
998        for entry in entries {
999            let bytes = encoded_entries(&[entry]);
1000            for invalid_frame in &invalid_frames {
1001                let mut malformed = bytes.clone();
1002                // Version, singleton entry count, and variant precede the frame word.
1003                malformed[3..3 + Word::min_serialized_size()]
1004                    .copy_from_slice(&invalid_frame.to_bytes());
1005                assert_eq!(
1006                    PrecompileWitness::read_from_bytes(&malformed),
1007                    Err(DeserializationError::InvalidValue("invalid deferred Eidos frame".into()))
1008                );
1009            }
1010        }
1011    }
1012
1013    #[test]
1014    fn decoder_rejects_overlong_entry_and_payload_lengths() {
1015        let entries = [
1016            WireEntry::Data {
1017                frame: deferred_chunks_frame(1),
1018                chunks: alloc::vec![felts(10)],
1019            },
1020            WireEntry::PairList {
1021                frame: frame(1),
1022                pairs: alloc::vec![(0, 0)],
1023            },
1024        ];
1025        for entry in entries {
1026            let witness = PrecompileWitness::from_entries(alloc::vec![entry]).unwrap();
1027            let bytes = witness.to_bytes();
1028            // Version + entry count + variant precede the frame and payload count.
1029            for offset in [1, 3 + Word::min_serialized_size()] {
1030                assert_eq!(bytes[offset], 3, "one uses the one-byte vint64 encoding");
1031                let mut noncanonical = bytes[..offset].to_vec();
1032                noncanonical.extend_from_slice(&[6, 0]);
1033                noncanonical.extend_from_slice(&bytes[offset + 1..]);
1034                assert!(PrecompileWitness::read_from_bytes(&noncanonical).is_err());
1035            }
1036        }
1037    }
1038
1039    #[test]
1040    fn singleton_vector_decodes_with_exact_byte_budget() {
1041        let witness = PrecompileWitness::from_entries(alloc::vec![WireEntry::Join {
1042            frame: DEFERRED_AND_FRAME,
1043            lhs: 0,
1044            rhs: 0
1045        }])
1046        .unwrap();
1047        assert_eq!(PrecompileWitness::min_serialized_size(), 2);
1048        assert!(PrecompileWitness::min_serialized_size() <= witness.to_bytes().len());
1049        let witnesses = alloc::vec![witness.clone(), witness];
1050        let bytes = witnesses.to_bytes();
1051        assert_eq!(
1052            Vec::<PrecompileWitness>::read_from_bytes_with_budget(&bytes, bytes.len()).unwrap(),
1053            witnesses
1054        );
1055    }
1056
1057    #[test]
1058    fn wire_element_budget_accepts_exact_limit_and_rejects_one_more() {
1059        let mut remaining = MAX_DEFERRED_WIRE_ELEMENTS;
1060        reserve_wire_elements(&mut remaining, MAX_DEFERRED_WIRE_ELEMENTS).unwrap();
1061        assert_eq!(remaining, 0);
1062        assert!(reserve_wire_elements(&mut remaining, 1).is_err());
1063        let mut overflow_budget = MAX_DEFERRED_WIRE_ELEMENTS;
1064        assert!(reserve_wire_payload(&mut overflow_budget, usize::MAX).is_err());
1065    }
1066
1067    #[test]
1068    fn in_memory_entries_enforce_the_wire_element_limit() {
1069        let join = WireEntry::Join {
1070            frame: DEFERRED_AND_FRAME,
1071            lhs: 0,
1072            rhs: 0,
1073        };
1074        let mut entries = alloc::vec![join; MAX_DEFERRED_WIRE_ELEMENTS / (EidosFrame::FELT_LEN + Node::DATA_CHUNK_FELT_LEN)];
1075        // Budget validation runs before hashing, so a repeated-entry allocation cannot bypass it.
1076        entries.push(WireEntry::Data {
1077            frame: deferred_chunks_frame(1),
1078            chunks: alloc::vec![felts(10)],
1079        });
1080        assert!(matches!(
1081            PrecompileWitness::from_entries(entries),
1082            Err(IntegrityError::DeferredStateTooLarge { .. })
1083        ));
1084    }
1085
1086    #[test]
1087    fn decoder_rejects_oversized_and_truncated_lengths_before_payload_allocation() {
1088        for count in [MAX_WIRE_ENTRIES, MAX_WIRE_ENTRIES + 1, usize::MAX] {
1089            let mut bytes = alloc::vec![PrecompileWitness::WIRE_VERSION];
1090            bytes.write_usize(count);
1091            assert!(PrecompileWitness::read_from_bytes(&bytes).is_err());
1092        }
1093        for discriminant in [0, 2] {
1094            let mut bytes = alloc::vec![PrecompileWitness::WIRE_VERSION];
1095            bytes.write_usize(1);
1096            bytes.write_u8(discriminant);
1097            frame(1).as_word().write_into(&mut bytes);
1098            bytes.write_usize(usize::MAX);
1099            assert!(PrecompileWitness::read_from_bytes(&bytes).is_err());
1100        }
1101    }
1102}
1103
1104// ARBITRARY (proptest)
1105// ================================================================================================
1106
1107#[cfg(any(test, feature = "arbitrary"))]
1108mod wire_arbitrary {
1109    use alloc::vec::Vec;
1110
1111    use miden_crypto::hash::eidos::{EidosDomain, EidosFrame};
1112    use proptest::prelude::*;
1113
1114    use super::{PrecompileWitness, WireEntry};
1115    use crate::program::domain::{
1116        CurvePrecompileDomain, Keccak256PrecompileDomain, Sha256PrecompileDomain,
1117        Sha512PrecompileDomain, Uint256PrecompileDomain,
1118    };
1119
1120    impl Arbitrary for PrecompileWitness {
1121        type Parameters = ();
1122        type Strategy = BoxedStrategy<Self>;
1123
1124        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1125            // Sound by construction: entry i (wire index i + 1) references earlier wire indices
1126            // such that every entry's flattened child list is ASCENDING and every entry is
1127            // reachable from the root (the LAST entry, which must be reference-bearing). Under
1128            // those two rules validate_structure's DFS emits entries in wire order, digests stay
1129            // unique (generated frames, differing fold structures), and the element limit holds. A
1130            // Data entry is allowed only at position 0 (it has no children, so it would orphan
1131            // its predecessors elsewhere); entry 1 references it. Five shapes are generated: the
1132            // linear Join chain (0, prev), the branching Join (prev - 1, prev) whose two
1133            // distinct children make the witness a non-linear DAG, the multi-pair PairList
1134            // [(0, prev), (prev, prev)], the single-pair PairList [(0, prev)], and the
1135            // position-0 Data entry. from_entries rejects the all-TRUE root, hence the witness
1136            // always has at least one reference-bearing entry.
1137            (1usize..=4)
1138                .prop_flat_map(|len| {
1139                    (
1140                        proptest::collection::vec(
1141                            (
1142                                prop_oneof![
1143                                    Just(Keccak256PrecompileDomain::TAG),
1144                                    Just(Uint256PrecompileDomain::TAG),
1145                                    Just(CurvePrecompileDomain::TAG),
1146                                    Just(Sha256PrecompileDomain::TAG),
1147                                    Just(Sha512PrecompileDomain::TAG),
1148                                ],
1149                                any::<[u32; 3]>(),
1150                            )
1151                                .prop_map(|(domain, params)| EidosFrame::new(domain, params)),
1152                            len,
1153                        ),
1154                        proptest::collection::vec(0u8..5, len),
1155                        proptest::collection::vec(
1156                            proptest::collection::vec(any::<crate::Felt>(), 8),
1157                            1..=3,
1158                        ),
1159                    )
1160                })
1161                .prop_map(|(frames, kinds, data_chunks)| {
1162                    // data_chunks is the (1..=3)-chunk payload of a position-0 Data entry; it is
1163                    // deliberately NOT zipped per entry so its length cannot truncate generation.
1164                    //
1165                    // Precompile domains accept every portable wire shape; the frame
1166                    // constructor guarantees canonical u32 parameters.
1167                    let n = frames.len();
1168                    let mut entries = Vec::with_capacity(n);
1169                    for (i, (frame, kind)) in frames.into_iter().zip(kinds).enumerate() {
1170                        if i == 0 && kind == 4 && n >= 2 {
1171                            let chunks = data_chunks
1172                                .iter()
1173                                .map(|chunk| chunk.clone().try_into().expect("8 felts per chunk"))
1174                                .collect();
1175                            entries.push(WireEntry::Data { frame, chunks });
1176                            continue;
1177                        }
1178                        let prev = i as u32; // the previous entry's wire index (TRUE for i = 0)
1179                        match kind % 4 {
1180                            0 => entries.push(WireEntry::Join { frame, lhs: 0, rhs: prev }),
1181                            // branching: two distinct earlier references (entries i - 2 and i - 1)
1182                            1 if i >= 2 => {
1183                                entries.push(WireEntry::Join { frame, lhs: prev - 1, rhs: prev });
1184                            },
1185                            1 => entries.push(WireEntry::Join { frame, lhs: 0, rhs: prev }),
1186                            // multi-pair: flattened children [0, prev, prev, prev] ascending
1187                            2 => entries.push(WireEntry::PairList {
1188                                frame,
1189                                pairs: alloc::vec![(0, prev), (prev, prev)],
1190                            }),
1191                            _ => {
1192                                entries.push(WireEntry::PairList {
1193                                    frame,
1194                                    pairs: alloc::vec![(0, prev)],
1195                                });
1196                            },
1197                        }
1198                    }
1199                    PrecompileWitness::from_entries(entries)
1200                        .expect("a mixed-kind witness DAG is canonical")
1201                })
1202                .boxed()
1203        }
1204    }
1205}