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. Every explicit entry references earlier entries, and the final
4//! entry opens the execution root. Decoding checks structure and commitments without evaluating
5//! precompile operations; operation support and assertion truth require evaluation.
6
7use alloc::{
8    collections::{BTreeMap, BTreeSet},
9    format,
10    sync::Arc,
11    vec::Vec,
12};
13
14use super::{
15    DataChunk, DeferredState, Digest, MAX_DEFERRED_ELEMENTS, Node, NodeType, PrecompileError,
16    PrecompileRegistry, TRUE_DIGEST, Tag, node::hash_payload,
17};
18use crate::{
19    Felt, ZERO,
20    serde::{
21        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
22        SliceReader, validate_bounded_len,
23    },
24};
25
26// CONSTANTS
27// ================================================================================================
28
29/// Reserved index for the always-known [`super::TRUE_DIGEST`] / [`super::Node::TRUE`] node.
30const TRUE_INDEX: u32 = 0;
31
32const MAX_WIRE_ENTRIES: usize = MAX_DEFERRED_ELEMENTS / Tag::FELT_LEN;
33
34fn reserve_wire_elements(
35    remaining_elements: &mut usize,
36    requested_elements: usize,
37) -> Result<(), DeserializationError> {
38    *remaining_elements = remaining_elements.checked_sub(requested_elements).ok_or_else(|| {
39        DeserializationError::InvalidValue(format!(
40            "deferred wire exceeds the {MAX_DEFERRED_ELEMENTS} element limit"
41        ))
42    })?;
43    Ok(())
44}
45
46fn reserve_wire_payload(
47    remaining_elements: &mut usize,
48    payload_count: usize,
49) -> Result<(), DeserializationError> {
50    let payload_elements =
51        payload_count.checked_mul(Node::DATA_CHUNK_FELT_LEN).ok_or_else(|| {
52            DeserializationError::InvalidValue("deferred wire element count overflow".into())
53        })?;
54    reserve_wire_elements(remaining_elements, payload_elements)
55}
56
57// WIRE ENTRY
58// ================================================================================================
59
60/// One explicit deferred DAG entry in topological wire order.
61///
62/// Wire index 0 is implicit TRUE. `entries[i]` has wire index `i + 1`. Structural children must
63/// reference `TRUE_INDEX` or an earlier entry. Pair-list pairs store structural child references in
64/// payload order.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum WireEntry {
67    /// Raw data payload interpreted by the tag's precompile.
68    ///
69    /// The payload requires at least one chunk. A tag's precompile may assign value semantics to a
70    /// one-chunk payload, but the wire shape itself does not.
71    Data { tag: Tag, chunks: Vec<DataChunk> },
72    /// Two child references resolved against `TRUE_INDEX` or earlier wire indices.
73    Join { tag: Tag, lhs: u32, rhs: u32 },
74    /// Raw structural child-reference pairs, with at least one pair.
75    PairList { tag: Tag, pairs: Vec<(u32, u32)> },
76}
77
78impl WireEntry {
79    // Join is the shortest valid entry; unchecked empty Data/PairList payloads are not witnesses.
80    fn min_serialized_size() -> usize {
81        1 + Tag::min_serialized_size() + 2 * u32::min_serialized_size()
82    }
83
84    /// Returns the tag whose operation is checked by the precompile prover.
85    pub fn tag(&self) -> Tag {
86        match self {
87            Self::Data { tag, .. } | Self::Join { tag, .. } | Self::PairList { tag, .. } => *tag,
88        }
89    }
90
91    fn children(&self) -> impl DoubleEndedIterator<Item = u32> + '_ {
92        let (join, pairs) = match self {
93            Self::Join { lhs, rhs, .. } => (Some([*lhs, *rhs]), &[][..]),
94            Self::PairList { pairs, .. } => (None, pairs.as_slice()),
95            Self::Data { .. } => (None, &[][..]),
96        };
97        join.into_iter()
98            .flatten()
99            .chain(pairs.iter().flat_map(|&(lhs, rhs)| [lhs, rhs]))
100    }
101
102    /// Reconstructs this entry's commitment from preceding digests, with TRUE at index zero.
103    ///
104    /// This validates framework shapes and references, without interpreting precompile tags or
105    /// evaluating assertions. The caller supplies only entries preceding this one.
106    pub fn digest(&self, digests: &[Digest]) -> Result<Digest, IntegrityError> {
107        if digests.first() != Some(&TRUE_DIGEST) {
108            return Err(IntegrityError::InvalidStructure);
109        }
110        let tag = self.tag();
111        if tag.is_framework_reserved()
112            && !matches!(self, Self::Data { tag, .. } if *tag == Tag::CHUNKS)
113            && !matches!(self, Self::Join { tag, .. } if *tag == Tag::AND)
114        {
115            return Err(IntegrityError::InvalidStructure);
116        }
117        if self.children().any(|index| index as usize >= digests.len()) {
118            return Err(IntegrityError::InvalidStructure);
119        }
120        let pair = |lhs: u32, rhs: u32| {
121            let lhs = digests[lhs as usize].into_elements();
122            let rhs = digests[rhs as usize].into_elements();
123            [lhs[0], lhs[1], lhs[2], lhs[3], rhs[0], rhs[1], rhs[2], rhs[3]]
124        };
125        match self {
126            Self::Data { chunks, .. } if !chunks.is_empty() => {
127                Ok(hash_payload(tag, chunks.iter().copied()))
128            },
129            Self::Join { lhs, rhs, .. } => Ok(hash_payload(tag, [pair(*lhs, *rhs)])),
130            Self::PairList { pairs, .. } if !pairs.is_empty() => {
131                Ok(hash_payload(tag, pairs.iter().map(|&(lhs, rhs)| pair(lhs, rhs))))
132            },
133            _ => Err(IntegrityError::InvalidStructure),
134        }
135    }
136}
137
138// PORTABLE WITNESS
139// ================================================================================================
140
141/// A portable opening of one nonempty deferred execution obligation.
142///
143/// Entries are canonical, child-first, duplicate-free, and reachable from the single non-TRUE
144/// root. A witness contains private prover input, without runtime state or evaluation caches.
145/// Structural validity does not establish operation support or assertion truth.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct PrecompileWitness {
148    entries: Vec<WireEntry>,
149    root: Digest,
150}
151
152impl PrecompileWitness {
153    /// Version of the standalone singleton witness encoding. Other versions are rejected.
154    pub const WIRE_VERSION: u8 = 1;
155
156    /// Checks a nonempty canonical singleton graph without evaluating any precompile.
157    pub fn from_entries(entries: Vec<WireEntry>) -> Result<Self, IntegrityError> {
158        let mut wire = Self { entries, root: TRUE_DIGEST };
159        wire.root = wire.validate_structure()?;
160        if wire.root == TRUE_DIGEST {
161            return Err(IntegrityError::InvalidStructure);
162        }
163        Ok(wire)
164    }
165
166    /// Returns the cached commitment without checking the precompile computations.
167    ///
168    /// Use [`Self::compute_root`] to evaluate the witness and recompute its commitment.
169    pub fn root_unchecked(&self) -> Digest {
170        self.root
171    }
172
173    /// Evaluates the witness under `registry` and returns its recomputed root commitment.
174    ///
175    /// Entries must reference only earlier entries or implicit TRUE at index zero. Each node is
176    /// registered in a temporary [`DeferredState`], which checks its shape and computation. The
177    /// final node must evaluate to TRUE. The cached root is not used.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error for an empty witness, invalid references, unsupported operations, failed
182    /// assertions, or evaluation that exceeds the deferred state budget.
183    pub fn compute_root(
184        &self,
185        registry: Arc<PrecompileRegistry>,
186    ) -> Result<Digest, PrecompileError> {
187        if self.entries.is_empty() {
188            return Err(PrecompileError::InvalidNode);
189        }
190        let mut state = DeferredState::new(registry)?;
191        let mut digests = Vec::with_capacity(self.entries.len() + 1);
192        digests.push(TRUE_DIGEST);
193        for entry in &self.entries {
194            let child = |index: u32| {
195                digests.get(index as usize).copied().ok_or(PrecompileError::InvalidNode)
196            };
197            let node = match entry {
198                WireEntry::Data { tag, chunks } => {
199                    if *tag == Tag::CHUNKS {
200                        Node::chunks(chunks.clone())?
201                    } else {
202                        Node::try_data(*tag, chunks.clone())?
203                    }
204                },
205                WireEntry::Join { tag, lhs, rhs } => {
206                    let (lhs, rhs) = (child(*lhs)?, child(*rhs)?);
207                    if *tag == Tag::AND {
208                        Node::and(lhs, rhs)
209                    } else {
210                        Node::join(*tag, lhs, rhs)?
211                    }
212                },
213                WireEntry::PairList { tag, pairs } => {
214                    let pairs = pairs
215                        .iter()
216                        .map(|&(lhs, rhs)| Ok((child(lhs)?, child(rhs)?)))
217                        .collect::<Result<Vec<_>, PrecompileError>>()?;
218                    Node::try_pair_list(*tag, pairs)?
219                },
220            };
221            digests.push(state.register(node)?);
222        }
223        let root = *digests.last().expect("TRUE seeds the digest table");
224        if state.evaluate_digest(root)? != TRUE_DIGEST {
225            return Err(PrecompileError::AssertionFailed);
226        }
227        Ok(root)
228    }
229
230    /// Returns canonical child-first entries. Index zero denotes implicit TRUE.
231    pub fn entries(&self) -> &[WireEntry] {
232        &self.entries
233    }
234
235    fn validate_structure(&self) -> Result<Digest, IntegrityError> {
236        self.validate_element_limit()?;
237        let mut digests = Vec::with_capacity(self.entries.len() + 1);
238        let mut seen_digests = BTreeSet::new();
239        digests.push(TRUE_DIGEST);
240        seen_digests.insert(TRUE_DIGEST);
241        for entry in &self.entries {
242            let digest = entry.digest(&digests)?;
243            if !seen_digests.insert(digest) {
244                return Err(IntegrityError::InvalidStructure);
245            }
246            digests.push(digest);
247        }
248
249        // The same left-to-right DFS used by the exporter must emit exactly the supplied stream.
250        // Backward references make this iterative traversal acyclic, including for shared graphs.
251        let mut seen = alloc::vec![false; digests.len()];
252        let mut pending = alloc::vec![(self.entries.len(), false)];
253        let mut next_index = 1;
254        while let Some((index, emit)) = pending.pop() {
255            if index == 0 {
256                continue;
257            }
258            if emit {
259                if index != next_index {
260                    return Err(IntegrityError::InvalidStructure);
261                }
262                next_index += 1;
263            } else if !core::mem::replace(&mut seen[index], true) {
264                pending.push((index, true));
265                pending.extend(
266                    self.entries[index - 1].children().rev().map(|child| (child as usize, false)),
267                );
268            }
269        }
270        if next_index != digests.len() {
271            return Err(IntegrityError::InvalidStructure);
272        }
273        Ok(*digests.last().expect("TRUE seeds the digest table"))
274    }
275
276    /// Exports the original root-reachable execution graph without serializing through bytes.
277    pub(crate) fn from_state(state: &DeferredState) -> Result<Self, IntegrityError> {
278        let mut build = WireEncoder::default();
279        build.visit_state_digest(state, state.root())?;
280        Ok(Self {
281            entries: build.entries,
282            root: state.root(),
283        })
284    }
285
286    fn validate_element_limit(&self) -> Result<(), IntegrityError> {
287        let mut remaining_elements = MAX_DEFERRED_ELEMENTS;
288        for entry in &self.entries {
289            let payload_count = match entry {
290                WireEntry::Data { chunks, .. } => chunks.len(),
291                WireEntry::Join { .. } => 1,
292                WireEntry::PairList { pairs, .. } => pairs.len(),
293            };
294            let payload_elements = payload_count
295                .checked_mul(Node::DATA_CHUNK_FELT_LEN)
296                .and_then(|elements| Tag::FELT_LEN.checked_add(elements))
297                .ok_or(IntegrityError::InvalidStructure)?;
298            remaining_elements = remaining_elements.checked_sub(payload_elements).ok_or(
299                IntegrityError::DeferredStateTooLarge {
300                    num_elements: payload_elements,
301                    max: remaining_elements,
302                },
303            )?;
304        }
305        Ok(())
306    }
307}
308
309// INTEGRITY ERROR
310// ================================================================================================
311
312/// A portable graph cannot be represented within the structural and resource constraints.
313#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
314pub enum IntegrityError {
315    /// Invalid framework shape, child reference, duplicate, orphan, or canonical entry order.
316    #[error("invalid or non-canonical precompile witness structure")]
317    InvalidStructure,
318    /// The portable entries exceed the same field-element ceiling as execution state.
319    #[error("deferred insertion requires {num_elements} elements but only {max} remain")]
320    DeferredStateTooLarge { num_elements: usize, max: usize },
321}
322
323// WIRE ENCODING
324// ================================================================================================
325
326/// Iterative exporter for the original, root-reachable execution graph.
327#[derive(Default)]
328struct WireEncoder {
329    seen: BTreeSet<Digest>,
330    by_digest: BTreeMap<Digest, u32>,
331    entries: Vec<WireEntry>,
332}
333
334impl WireEncoder {
335    fn visit_state_digest(
336        &mut self,
337        state: &DeferredState,
338        digest: Digest,
339    ) -> Result<(), IntegrityError> {
340        let mut pending = Vec::new();
341        pending.push(WireEncodeStep::Visit(digest));
342
343        while let Some(step) = pending.pop() {
344            match step {
345                WireEncodeStep::Visit(digest) => {
346                    self.schedule_digest(state, digest, &mut pending)?
347                },
348                WireEncodeStep::Emit(digest) => {
349                    let entry = self.entry_for_digest(state, digest)?;
350                    self.push_entry(digest, entry)?;
351                },
352            }
353        }
354
355        Ok(())
356    }
357
358    fn schedule_digest(
359        &mut self,
360        state: &DeferredState,
361        digest: Digest,
362        pending: &mut Vec<WireEncodeStep>,
363    ) -> Result<(), IntegrityError> {
364        if digest == TRUE_DIGEST || !self.seen.insert(digest) {
365            return Ok(());
366        }
367
368        let node = self.validated_node(state, digest)?;
369        pending.push(WireEncodeStep::Emit(digest));
370
371        match self.node_type(state, node)? {
372            NodeType::Data => {},
373            NodeType::Join => {
374                let (lhs, rhs) =
375                    node.payload().as_join().map_err(|_| IntegrityError::InvalidStructure)?;
376                pending.push(WireEncodeStep::Visit(rhs));
377                pending.push(WireEncodeStep::Visit(lhs));
378            },
379            NodeType::PairList => {
380                let pairs =
381                    node.payload().as_pair_list().map_err(|_| IntegrityError::InvalidStructure)?;
382                for (lhs, rhs) in pairs.iter().rev() {
383                    pending.push(WireEncodeStep::Visit(*rhs));
384                    pending.push(WireEncodeStep::Visit(*lhs));
385                }
386            },
387            NodeType::True => return Err(IntegrityError::InvalidStructure),
388        };
389
390        Ok(())
391    }
392
393    fn entry_for_digest(
394        &self,
395        state: &DeferredState,
396        digest: Digest,
397    ) -> Result<WireEntry, IntegrityError> {
398        let node = self.validated_node(state, digest)?;
399
400        Ok(match self.node_type(state, node)? {
401            NodeType::Data => WireEntry::Data {
402                tag: node.tag(),
403                chunks: node
404                    .payload()
405                    .as_data()
406                    .map_err(|_| IntegrityError::InvalidStructure)?
407                    .to_vec(),
408            },
409            NodeType::Join => {
410                let (lhs, rhs) =
411                    node.payload().as_join().map_err(|_| IntegrityError::InvalidStructure)?;
412                let lhs = self.index_for(lhs)?;
413                let rhs = self.index_for(rhs)?;
414                WireEntry::Join { tag: node.tag(), lhs, rhs }
415            },
416            NodeType::PairList => {
417                let pairs =
418                    node.payload().as_pair_list().map_err(|_| IntegrityError::InvalidStructure)?;
419                let pairs = pairs
420                    .iter()
421                    .map(|(lhs, rhs)| Ok((self.index_for(*lhs)?, self.index_for(*rhs)?)))
422                    .collect::<Result<Vec<_>, IntegrityError>>()?;
423                WireEntry::PairList { tag: node.tag(), pairs }
424            },
425            NodeType::True => return Err(IntegrityError::InvalidStructure),
426        })
427    }
428
429    fn validated_node<'a>(
430        &self,
431        state: &'a DeferredState,
432        digest: Digest,
433    ) -> Result<&'a Node, IntegrityError> {
434        let node = state.get_node(&digest).ok_or(IntegrityError::InvalidStructure)?;
435        self.node_type(state, node)?
436            .validate_node(node)
437            .map_err(|_| IntegrityError::InvalidStructure)?;
438        Ok(node)
439    }
440
441    fn node_type(&self, state: &DeferredState, node: &Node) -> Result<NodeType, IntegrityError> {
442        state
443            .registry()
444            .decode_node_type(node.tag())
445            .map_err(|_| IntegrityError::InvalidStructure)
446    }
447
448    fn index_for(&self, digest: Digest) -> Result<u32, IntegrityError> {
449        if digest == TRUE_DIGEST {
450            return Ok(TRUE_INDEX);
451        }
452        self.by_digest.get(&digest).copied().ok_or(IntegrityError::InvalidStructure)
453    }
454
455    fn push_entry(&mut self, digest: Digest, entry: WireEntry) -> Result<(), IntegrityError> {
456        let next_index =
457            self.entries.len().checked_add(1).ok_or(IntegrityError::InvalidStructure)?;
458        let next_index = u32::try_from(next_index).map_err(|_| IntegrityError::InvalidStructure)?;
459        self.entries.push(entry);
460        self.by_digest.insert(digest, next_index);
461        Ok(())
462    }
463}
464
465enum WireEncodeStep {
466    Visit(Digest),
467    Emit(Digest),
468}
469
470// SERIALIZATION
471// ================================================================================================
472
473impl Serializable for WireEntry {
474    fn write_into<W: ByteWriter>(&self, target: &mut W) {
475        match self {
476            Self::Data { tag, chunks } => {
477                target.write_u8(0);
478                tag.write_into(target);
479                target.write_usize(chunks.len());
480                for chunk in chunks {
481                    for felt in chunk {
482                        felt.write_into(target);
483                    }
484                }
485            },
486            Self::Join { tag, lhs, rhs } => {
487                target.write_u8(1);
488                tag.write_into(target);
489                target.write_u32(*lhs);
490                target.write_u32(*rhs);
491            },
492            Self::PairList { tag, pairs } => {
493                target.write_u8(2);
494                tag.write_into(target);
495                target.write_usize(pairs.len());
496                for (lhs, rhs) in pairs {
497                    target.write_u32(*lhs);
498                    target.write_u32(*rhs);
499                }
500            },
501        }
502    }
503}
504
505struct WirePair((u32, u32));
506
507impl Deserializable for WirePair {
508    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
509        Ok(Self((source.read_u32()?, source.read_u32()?)))
510    }
511
512    fn min_serialized_size() -> usize {
513        u32::min_serialized_size() * 2
514    }
515}
516
517struct WireDataChunk(DataChunk);
518
519impl Deserializable for WireDataChunk {
520    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
521        let mut chunk = [ZERO; Node::DATA_CHUNK_FELT_LEN];
522        for felt in &mut chunk {
523            *felt = Felt::read_from(source)?;
524        }
525        Ok(Self(chunk))
526    }
527
528    fn min_serialized_size() -> usize {
529        Node::DATA_CHUNK_FELT_LEN * Felt::min_serialized_size()
530    }
531}
532
533impl Serializable for PrecompileWitness {
534    fn write_into<W: ByteWriter>(&self, target: &mut W) {
535        target.write_u8(Self::WIRE_VERSION);
536        target.write_usize(self.entries.len());
537        for entry in &self.entries {
538            entry.write_into(target);
539        }
540    }
541}
542
543impl Deserializable for PrecompileWitness {
544    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
545        let version = source.read_u8()?;
546        if version != Self::WIRE_VERSION {
547            return Err(DeserializationError::InvalidValue(format!(
548                "unsupported precompile witness version {version} (expected {})",
549                Self::WIRE_VERSION
550            )));
551        }
552        let entry_count =
553            read_len(source, "precompile witness entry", WireEntry::min_serialized_size())?;
554        if entry_count == 0 || entry_count > MAX_WIRE_ENTRIES {
555            return Err(DeserializationError::InvalidValue(format!(
556                "precompile witness contains {entry_count} entries, expected 1..={MAX_WIRE_ENTRIES}"
557            )));
558        }
559
560        let mut remaining_elements = MAX_DEFERRED_ELEMENTS;
561        let mut entries = Vec::with_capacity(entry_count);
562        for _ in 0..entry_count {
563            entries.push(read_wire_entry(source, &mut remaining_elements)?);
564        }
565        Self::from_entries(entries).map_err(|error| {
566            DeserializationError::InvalidValue(format!("invalid precompile witness: {error}"))
567        })
568    }
569
570    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
571        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
572        let wire = Self::read_from(&mut reader)?;
573        if reader.has_more_bytes() {
574            return Err(DeserializationError::InvalidValue(
575                "trailing bytes after deferred witness".into(),
576            ));
577        }
578        Ok(wire)
579    }
580
581    fn min_serialized_size() -> usize {
582        1 + usize::min_serialized_size() + WireEntry::min_serialized_size()
583    }
584}
585
586/// Retain the existing vint64 decoder and allocation checks, requiring its shortest encoding.
587fn read_len<R: ByteReader>(
588    source: &mut R,
589    label: &str,
590    min_element_size: usize,
591) -> Result<usize, DeserializationError> {
592    let encoded_len = source.peek_u8()?.trailing_zeros() as usize + 1;
593    let len = source.read_usize()?;
594    // The usize Serializable implementation computes the exact vint64 width without allocating.
595    if encoded_len != len.get_size_hint() {
596        return Err(DeserializationError::InvalidValue(format!("noncanonical {label} length")));
597    }
598    validate_bounded_len(source, label, len, min_element_size)?;
599    Ok(len)
600}
601
602fn read_wire_entry<R: ByteReader>(
603    source: &mut R,
604    remaining_elements: &mut usize,
605) -> Result<WireEntry, DeserializationError> {
606    let discriminant = source.read_u8()?;
607    match discriminant {
608        0 => {
609            reserve_wire_elements(remaining_elements, Tag::FELT_LEN)?;
610            let tag = Tag::read_from(source)?;
611            let chunk_count = read_len(source, "data chunk", WireDataChunk::min_serialized_size())?;
612            reserve_wire_payload(remaining_elements, chunk_count)?;
613            let chunks = source
614                .read_many_iter::<WireDataChunk>(chunk_count)?
615                .map(|chunk| chunk.map(|chunk| chunk.0))
616                .collect::<Result<_, _>>()?;
617            Ok(WireEntry::Data { tag, chunks })
618        },
619        1 => {
620            reserve_wire_elements(remaining_elements, Tag::FELT_LEN + Node::DATA_CHUNK_FELT_LEN)?;
621            let tag = Tag::read_from(source)?;
622            let lhs = source.read_u32()?;
623            let rhs = source.read_u32()?;
624            Ok(WireEntry::Join { tag, lhs, rhs })
625        },
626        2 => {
627            reserve_wire_elements(remaining_elements, Tag::FELT_LEN)?;
628            let tag = Tag::read_from(source)?;
629            let pair_count = read_len(source, "child pair", WirePair::min_serialized_size())?;
630            reserve_wire_payload(remaining_elements, pair_count)?;
631            let pairs = source
632                .read_many_iter::<WirePair>(pair_count)?
633                .map(|pair| pair.map(|pair| pair.0))
634                .collect::<Result<_, _>>()?;
635            Ok(WireEntry::PairList { tag, pairs })
636        },
637        other => Err(DeserializationError::InvalidValue(format!(
638            "invalid deferred wire entry discriminant: {other}"
639        ))),
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    fn felts(seed: u64) -> DataChunk {
648        core::array::from_fn(|i| Felt::new_unchecked(seed + i as u64))
649    }
650
651    fn tag(seed: u64) -> Tag {
652        Tag::from_word([Felt::new_unchecked(seed + 100), ZERO, ZERO, ZERO])
653    }
654
655    fn encoded_entries(entries: &[WireEntry]) -> Vec<u8> {
656        let mut bytes = alloc::vec![PrecompileWitness::WIRE_VERSION];
657        bytes.write_usize(entries.len());
658        for entry in entries {
659            entry.write_into(&mut bytes);
660        }
661        bytes
662    }
663
664    #[test]
665    fn portable_structure_is_independent_of_operation_support() {
666        let entries = alloc::vec![
667            WireEntry::Data {
668                tag: tag(1),
669                chunks: alloc::vec![felts(10)]
670            },
671            WireEntry::Data {
672                tag: tag(2),
673                chunks: alloc::vec![felts(20), felts(30)]
674            },
675            WireEntry::Join { tag: tag(3), lhs: 1, rhs: 1 },
676            WireEntry::PairList {
677                tag: tag(4),
678                pairs: alloc::vec![(1, 2), (3, 3)]
679            },
680        ];
681        let witness = PrecompileWitness::from_entries(entries).unwrap();
682        let left = Node::value(tag(1), felts(10)).unwrap().digest();
683        let right = Node::try_data(tag(2), alloc::vec![felts(20), felts(30)]).unwrap().digest();
684        let claim = Node::join(tag(3), left, left).unwrap().digest();
685        assert_eq!(
686            witness.root_unchecked(),
687            Node::try_pair_list(tag(4), alloc::vec![(left, right), (claim, claim)])
688                .unwrap()
689                .digest()
690        );
691        assert_eq!(PrecompileWitness::read_from_bytes(&witness.to_bytes()).unwrap(), witness);
692    }
693
694    #[test]
695    fn portable_structure_rejects_noncanonical_and_malformed_graphs() {
696        let leaf = || WireEntry::Data {
697            tag: tag(1),
698            chunks: alloc::vec![felts(10)],
699        };
700        let other = || WireEntry::Data {
701            tag: tag(1),
702            chunks: alloc::vec![felts(20)],
703        };
704        let malformed_and = Tag::from_word([Tag::AND.id(), Felt::new_unchecked(1), ZERO, ZERO]);
705        let cases = [
706            Vec::new(),
707            alloc::vec![WireEntry::Data { tag: Tag::CHUNKS, chunks: Vec::new() }],
708            alloc::vec![WireEntry::PairList { tag: tag(1), pairs: Vec::new() }],
709            alloc::vec![WireEntry::Data {
710                tag: Tag::TRUE,
711                chunks: alloc::vec![felts(10)]
712            }],
713            alloc::vec![WireEntry::Join { tag: Tag::CHUNKS, lhs: 0, rhs: 0 }],
714            alloc::vec![WireEntry::Join { tag: malformed_and, lhs: 0, rhs: 0 }],
715            alloc::vec![WireEntry::Join { tag: Tag::AND, lhs: 0, rhs: 1 }],
716            alloc::vec![leaf(), leaf(), WireEntry::Join { tag: Tag::AND, lhs: 1, rhs: 2 }],
717            alloc::vec![leaf(), other()],
718            alloc::vec![leaf(), other(), WireEntry::Join { tag: Tag::AND, lhs: 2, rhs: 1 }],
719        ];
720        for entries in cases {
721            let bytes = encoded_entries(&entries);
722            assert!(PrecompileWitness::from_entries(entries).is_err());
723            assert!(PrecompileWitness::read_from_bytes(&bytes).is_err());
724        }
725    }
726
727    #[test]
728    fn compute_root_ignores_cached_root_and_checks_entries() {
729        let mut witness = PrecompileWitness::from_entries(alloc::vec![WireEntry::Join {
730            tag: Tag::AND,
731            lhs: 0,
732            rhs: 0,
733        }])
734        .unwrap();
735        let expected = witness.root_unchecked();
736        witness.root = TRUE_DIGEST;
737        let registry = Arc::new(PrecompileRegistry::new());
738        assert_eq!(witness.compute_root(registry.clone()).unwrap(), expected);
739
740        for entries in [
741            Vec::new(),
742            alloc::vec![WireEntry::Join { tag: Tag::AND, lhs: 0, rhs: 1 }],
743            alloc::vec![
744                WireEntry::Join { tag: Tag::AND, lhs: 0, rhs: 2 },
745                WireEntry::Join { tag: Tag::AND, lhs: 0, rhs: 0 },
746            ],
747            alloc::vec![WireEntry::PairList { tag: tag(1), pairs: alloc::vec![(0, 1)] }],
748        ] {
749            let malformed = PrecompileWitness { entries, root: expected };
750            assert!(matches!(
751                malformed.compute_root(registry.clone()),
752                Err(PrecompileError::InvalidNode)
753            ));
754        }
755        let value = PrecompileWitness::from_entries(alloc::vec![WireEntry::Data {
756            tag: Tag::CHUNKS,
757            chunks: alloc::vec![felts(10)],
758        }])
759        .unwrap();
760        assert!(matches!(value.compute_root(registry), Err(PrecompileError::AssertionFailed)));
761    }
762
763    #[test]
764    fn export_omits_unreachable_state_and_retains_logged_true() {
765        let mut empty = DeferredState::default();
766        empty.register(Node::chunks(alloc::vec![felts(10)]).unwrap()).unwrap();
767        assert!(empty.into_witness().unwrap().is_none());
768        let mut state = DeferredState::default();
769        state.register(Node::chunks(alloc::vec![felts(10)]).unwrap()).unwrap();
770        state.log_statement(TRUE_DIGEST).unwrap();
771        let root = state.root();
772        let witness = state.into_witness().unwrap().unwrap();
773        assert_eq!(witness.root_unchecked(), root);
774        assert_eq!(witness.entries(), &[WireEntry::Join { tag: Tag::AND, lhs: 0, rhs: 0 }]);
775    }
776
777    #[test]
778    fn deep_shared_graph_exports_and_decodes_without_expansion() {
779        let mut state = DeferredState::default();
780        let mut statement = TRUE_DIGEST;
781        for _ in 0..4_096 {
782            statement = state.register(Node::and(statement, statement)).unwrap();
783        }
784        state.log_statement(statement).unwrap();
785        let root = state.root();
786        let witness = state.into_witness().unwrap().unwrap();
787        assert_eq!(witness.entries().len(), 4_097);
788        assert_eq!(witness.compute_root(Arc::new(PrecompileRegistry::new())).unwrap(), root);
789        assert_eq!(witness.root_unchecked(), root);
790        assert_eq!(PrecompileWitness::from_entries(witness.entries().to_vec()).unwrap(), witness);
791        assert_eq!(PrecompileWitness::read_from_bytes(&witness.to_bytes()).unwrap(), witness);
792    }
793
794    #[test]
795    fn standalone_witness_rejects_unsupported_versions_and_trailing_bytes() {
796        let witness = PrecompileWitness::from_entries(alloc::vec![WireEntry::Join {
797            tag: Tag::AND,
798            lhs: 0,
799            rhs: 0
800        }])
801        .unwrap();
802        let bytes = witness.to_bytes();
803        for version in [0, PrecompileWitness::WIRE_VERSION + 1] {
804            let mut unsupported = bytes.clone();
805            unsupported[0] = version;
806            assert!(PrecompileWitness::read_from_bytes(&unsupported).is_err());
807        }
808        let mut trailing = bytes;
809        trailing.push(0);
810        assert!(PrecompileWitness::read_from_bytes(&trailing).is_err());
811    }
812
813    #[test]
814    fn decoder_rejects_overlong_entry_and_payload_lengths() {
815        let entries = [
816            WireEntry::Data {
817                tag: Tag::CHUNKS,
818                chunks: alloc::vec![felts(10)],
819            },
820            WireEntry::PairList { tag: tag(1), pairs: alloc::vec![(0, 0)] },
821        ];
822        for entry in entries {
823            let witness = PrecompileWitness::from_entries(alloc::vec![entry]).unwrap();
824            let bytes = witness.to_bytes();
825            // Version + entry count + variant precede the tag and payload count.
826            for offset in [1, 3 + Tag::min_serialized_size()] {
827                assert_eq!(bytes[offset], 3, "one uses the one-byte vint64 encoding");
828                let mut noncanonical = bytes[..offset].to_vec();
829                noncanonical.extend_from_slice(&[6, 0]);
830                noncanonical.extend_from_slice(&bytes[offset + 1..]);
831                assert!(PrecompileWitness::read_from_bytes(&noncanonical).is_err());
832            }
833        }
834    }
835
836    #[test]
837    fn singleton_vector_decodes_with_exact_byte_budget() {
838        let witness = PrecompileWitness::from_entries(alloc::vec![WireEntry::Join {
839            tag: Tag::AND,
840            lhs: 0,
841            rhs: 0
842        }])
843        .unwrap();
844        assert_eq!(PrecompileWitness::min_serialized_size(), witness.to_bytes().len());
845        let witnesses = alloc::vec![witness.clone(), witness];
846        let bytes = witnesses.to_bytes();
847        assert_eq!(
848            Vec::<PrecompileWitness>::read_from_bytes_with_budget(&bytes, bytes.len()).unwrap(),
849            witnesses
850        );
851    }
852
853    #[test]
854    fn wire_element_budget_accepts_exact_limit_and_rejects_one_more() {
855        let mut remaining = MAX_DEFERRED_ELEMENTS;
856        reserve_wire_elements(&mut remaining, MAX_DEFERRED_ELEMENTS).unwrap();
857        assert_eq!(remaining, 0);
858        assert!(reserve_wire_elements(&mut remaining, 1).is_err());
859        let mut overflow_budget = MAX_DEFERRED_ELEMENTS;
860        assert!(reserve_wire_payload(&mut overflow_budget, usize::MAX).is_err());
861    }
862
863    #[test]
864    fn in_memory_entries_enforce_the_execution_element_limit() {
865        let join = WireEntry::Join { tag: Tag::AND, lhs: 0, rhs: 0 };
866        let mut entries =
867            alloc::vec![join; MAX_DEFERRED_ELEMENTS / (Tag::FELT_LEN + Node::DATA_CHUNK_FELT_LEN)];
868        // Budget validation runs before hashing, so a repeated-entry allocation cannot bypass it.
869        entries.push(WireEntry::Data {
870            tag: Tag::CHUNKS,
871            chunks: alloc::vec![felts(10)],
872        });
873        assert!(matches!(
874            PrecompileWitness::from_entries(entries),
875            Err(IntegrityError::DeferredStateTooLarge { .. })
876        ));
877    }
878
879    #[test]
880    fn decoder_rejects_oversized_and_truncated_lengths_before_payload_allocation() {
881        for count in [MAX_WIRE_ENTRIES, MAX_WIRE_ENTRIES + 1, usize::MAX] {
882            let mut bytes = alloc::vec![PrecompileWitness::WIRE_VERSION];
883            bytes.write_usize(count);
884            assert!(PrecompileWitness::read_from_bytes(&bytes).is_err());
885        }
886        for discriminant in [0, 2] {
887            let mut bytes = alloc::vec![PrecompileWitness::WIRE_VERSION];
888            bytes.write_usize(1);
889            bytes.write_u8(discriminant);
890            tag(1).write_into(&mut bytes);
891            bytes.write_usize(usize::MAX);
892            assert!(PrecompileWitness::read_from_bytes(&bytes).is_err());
893        }
894    }
895}