Skip to main content

miden_core/deferred/
wire.rs

1//! Compact wire format for deferred-state witnesses.
2//!
3//! This passive transport format is a canonical, topologically ordered stream of the explicit DAG
4//! entries needed to reconstruct a deferred state without hydrating it during proof decoding.
5//! Wire index 0 is reserved for the implicit TRUE node; entry `i` has wire index `i + 1`, and
6//! structural child references may only point to TRUE or earlier entries. Empty wire opens
7//! [`TRUE_DIGEST`];
8//! otherwise the root is the digest of the final entry.
9//!
10//! Rehydration decodes the untrusted stream into ordinary [`DeferredState`] nodes, rejects
11//! non-canonical/dangling wire by comparing with [`DeferredState::to_wire`], and finally evaluates
12//! the implicit root to repopulate evaluation memos.
13
14use alloc::{
15    collections::{BTreeMap, BTreeSet},
16    format,
17    sync::Arc,
18    vec::Vec,
19};
20
21use super::{
22    DataChunk, DeferredError, DeferredState, Digest, MAX_DEFERRED_ELEMENTS, Node, NodeType,
23    PrecompileError, PrecompileRegistry, TRUE_DIGEST, Tag,
24};
25use crate::{
26    Felt, ZERO,
27    serde::{
28        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
29        SliceReader,
30    },
31};
32
33// CONSTANTS
34// ================================================================================================
35
36/// Reserved index for the always-known [`super::TRUE_DIGEST`] / [`super::Node::TRUE`] node.
37const TRUE_INDEX: u32 = 0;
38
39const MAX_WIRE_ENTRIES: usize = MAX_DEFERRED_ELEMENTS / Tag::FELT_LEN;
40
41fn reserve_wire_elements(
42    remaining_elements: &mut usize,
43    requested_elements: usize,
44) -> Result<(), DeserializationError> {
45    *remaining_elements = remaining_elements.checked_sub(requested_elements).ok_or_else(|| {
46        DeserializationError::InvalidValue(format!(
47            "deferred wire exceeds the {MAX_DEFERRED_ELEMENTS} element limit"
48        ))
49    })?;
50    Ok(())
51}
52
53fn reserve_wire_payload(
54    remaining_elements: &mut usize,
55    payload_count: usize,
56) -> Result<(), DeserializationError> {
57    let payload_elements =
58        payload_count.checked_mul(Node::DATA_CHUNK_FELT_LEN).ok_or_else(|| {
59            DeserializationError::InvalidValue("deferred wire element count overflow".into())
60        })?;
61    reserve_wire_elements(remaining_elements, payload_elements)
62}
63
64// WIRE ENTRY
65// ================================================================================================
66
67/// One explicit deferred DAG entry in topological wire order.
68///
69/// Wire index 0 is implicit TRUE. `entries[i]` has wire index `i + 1`. Structural children must
70/// reference `TRUE_INDEX` or an earlier entry. Pair-list pairs store structural child references in
71/// payload order.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub(crate) enum WireEntry {
74    /// Raw data payload interpreted by the tag's precompile.
75    ///
76    /// Rehydration requires at least one chunk. A tag's precompile may assign value semantics to a
77    /// one-chunk payload, but the wire shape itself does not.
78    Data { tag: Tag, chunks: Vec<DataChunk> },
79    /// Two child references resolved against `TRUE_INDEX` or earlier wire indices.
80    Join { tag: Tag, lhs: u32, rhs: u32 },
81    /// Raw structural child-reference pairs. Rehydration requires at least one pair.
82    PairList { tag: Tag, pairs: Vec<(u32, u32)> },
83}
84
85// DEFERRED STATE WIRE
86// ================================================================================================
87
88/// Wire representation of a deferred root opening.
89///
90/// The root is implicit: empty `entries` opens [`TRUE_DIGEST`], otherwise the root is the digest of
91/// the last entry. Accepted wire must be topologically ordered, root-last, duplicate-free,
92/// canonical, and semantically valid under the installed [`PrecompileRegistry`] when rehydrated.
93#[derive(Debug, Clone, PartialEq, Eq, Default)]
94pub struct DeferredStateWire {
95    entries: Vec<WireEntry>,
96}
97
98impl DeferredStateWire {
99    /// Serializes the root-reachable DAG into deterministic wire form.
100    pub(crate) fn from_state(state: &DeferredState) -> Result<Self, IntegrityError> {
101        let mut build = WireEncoder::default();
102        build.visit_state_digest(state, state.root())?;
103        Ok(Self { entries: build.entries })
104    }
105
106    /// Rebuilds and verifies a deferred state from untrusted wire data.
107    pub(crate) fn rehydrate(
108        &self,
109        precompiles: Arc<PrecompileRegistry>,
110    ) -> Result<DeferredState, IntegrityError> {
111        self.validate_element_limit()?;
112        let (entries, root) = WireDecoder::new(self, precompiles.as_ref())?.decode()?;
113        let mut state = DeferredState::new(Arc::clone(&precompiles))?;
114
115        // Register entries in strict topological wire order. Structural children have already been
116        // decoded to earlier digests, so ordinary DeferredState registration enforces the
117        // same child-closure and budget rules as execution.
118        for (digest, node) in entries {
119            let registered = state.register(node)?;
120            if registered != digest {
121                return Err(IntegrityError::InvalidStructure);
122            }
123        }
124
125        state.root = root;
126
127        // `to_wire` emits the deterministic root-reachable closure. Equality makes the accepted
128        // format strict: root-last, canonical DFS order, and no dangling or duplicate
129        // entries.
130        if state.to_wire()? != *self {
131            return Err(IntegrityError::InvalidStructure);
132        }
133
134        if state.evaluate_digest(root)? != TRUE_DIGEST {
135            return Err(IntegrityError::RootNotTrue);
136        }
137
138        Ok(state)
139    }
140
141    fn validate_element_limit(&self) -> Result<(), IntegrityError> {
142        let mut remaining_elements = MAX_DEFERRED_ELEMENTS;
143        for entry in &self.entries {
144            let payload_count = match entry {
145                WireEntry::Data { chunks, .. } => chunks.len(),
146                WireEntry::Join { .. } => 1,
147                WireEntry::PairList { pairs, .. } => pairs.len(),
148            };
149            let payload_elements = payload_count
150                .checked_mul(Node::DATA_CHUNK_FELT_LEN)
151                .and_then(|elements| Tag::FELT_LEN.checked_add(elements))
152                .ok_or(IntegrityError::InvalidStructure)?;
153            remaining_elements = remaining_elements.checked_sub(payload_elements).ok_or(
154                IntegrityError::DeferredStateTooLarge {
155                    num_elements: payload_elements,
156                    max: remaining_elements,
157                },
158            )?;
159        }
160        Ok(())
161    }
162}
163
164// INTEGRITY ERROR
165// ================================================================================================
166
167/// Reasons untrusted wire data failed deferred-state rehydration.
168///
169/// Any variant rejects the proof witness under the installed `PrecompileRegistry`. Structural wire
170/// details intentionally collapse into [`Self::InvalidStructure`]; callers only need to distinguish
171/// malformed/non-canonical openings, root mismatches, semantic root failures, and budget failures.
172/// The enum is not `Clone`/`Eq` because evaluation failures carry opaque precompile errors.
173#[derive(Debug, thiserror::Error)]
174pub enum IntegrityError {
175    /// The wire/state structure is malformed or not the canonical root-last opening.
176    #[error("invalid or non-canonical deferred wire/state structure")]
177    InvalidStructure,
178    /// Root evaluation failed under the installed precompile registry.
179    #[error("deferred root failed evaluation: {0}")]
180    EvaluationFailed(#[source] PrecompileError),
181    /// The root evaluated, but not to the canonical TRUE node.
182    #[error("deferred root evaluated to a non-TRUE canonical form")]
183    RootNotTrue,
184    /// Rehydrating the wire would exceed the fixed deferred-state budget.
185    #[error("deferred insertion requires {num_elements} elements but only {max} remain")]
186    DeferredStateTooLarge { num_elements: usize, max: usize },
187}
188
189impl From<PrecompileError> for IntegrityError {
190    fn from(err: PrecompileError) -> Self {
191        if let PrecompileError::Other(DeferredError::DeferredStateTooLarge { num_elements, max }) =
192            err.root()
193        {
194            Self::DeferredStateTooLarge { num_elements: *num_elements, max: *max }
195        } else {
196            Self::EvaluationFailed(err)
197        }
198    }
199}
200
201// WIRE REHYDRATION
202// ================================================================================================
203
204struct WireDecoder<'a> {
205    wire: &'a DeferredStateWire,
206    precompiles: &'a PrecompileRegistry,
207    entries: Vec<(Digest, Node)>,
208    index_to_digest: Vec<Digest>,
209    seen_digests: BTreeSet<Digest>,
210}
211
212impl<'a> WireDecoder<'a> {
213    fn new(
214        wire: &'a DeferredStateWire,
215        precompiles: &'a PrecompileRegistry,
216    ) -> Result<Self, IntegrityError> {
217        let total_nodes =
218            1usize.checked_add(wire.entries.len()).ok_or(IntegrityError::InvalidStructure)?;
219
220        let mut index_to_digest = Vec::with_capacity(total_nodes);
221        let mut seen_digests = BTreeSet::new();
222        index_to_digest.push(TRUE_DIGEST);
223        seen_digests.insert(TRUE_DIGEST);
224
225        Ok(Self {
226            wire,
227            precompiles,
228            entries: Vec::with_capacity(wire.entries.len()),
229            index_to_digest,
230            seen_digests,
231        })
232    }
233
234    fn decode(mut self) -> Result<(Vec<(Digest, Node)>, Digest), IntegrityError> {
235        for entry in &self.wire.entries {
236            let node = match entry {
237                WireEntry::Data { tag, chunks } => self.decode_data_entry(*tag, chunks)?,
238                WireEntry::Join { tag, lhs, rhs } => self.decode_join_entry(*tag, *lhs, *rhs)?,
239                WireEntry::PairList { tag, pairs } => self.decode_pair_list_entry(*tag, pairs)?,
240            };
241            self.push_entry(node)?;
242        }
243
244        let root = *self.index_to_digest.last().expect("digest table is seeded with TRUE_DIGEST");
245        Ok((self.entries, root))
246    }
247
248    fn decode_data_entry(&self, tag: Tag, chunks: &[DataChunk]) -> Result<Node, IntegrityError> {
249        // The decoded shape — not the wire entry variant — decides whether these payload bytes are
250        // data. Semantic chunk-count checks belong to the owning precompile during registration.
251        let node_type = self
252            .precompiles
253            .decode_node_type(tag)
254            .map_err(|_| IntegrityError::InvalidStructure)?;
255        let NodeType::Data = node_type else {
256            return Err(IntegrityError::InvalidStructure);
257        };
258        let node = if tag == Tag::CHUNKS {
259            Node::chunks(chunks.to_vec()).map_err(|_| IntegrityError::InvalidStructure)?
260        } else {
261            Node::try_data(tag, chunks.to_vec()).map_err(|_| IntegrityError::InvalidStructure)?
262        };
263        node_type.validate_node(&node).map_err(|_| IntegrityError::InvalidStructure)?;
264        Ok(node)
265    }
266
267    fn decode_join_entry(&self, tag: Tag, lhs: u32, rhs: u32) -> Result<Node, IntegrityError> {
268        let lhs = self.resolve_index(lhs)?;
269        let rhs = self.resolve_index(rhs)?;
270        let node = if tag == Tag::AND {
271            Node::and(lhs, rhs)
272        } else {
273            Node::join(tag, lhs, rhs).map_err(|_| IntegrityError::InvalidStructure)?
274        };
275        let node_type = self
276            .precompiles
277            .decode_node_type(node.tag())
278            .map_err(|_| IntegrityError::InvalidStructure)?;
279        node_type.validate_node(&node).map_err(|_| IntegrityError::InvalidStructure)?;
280        match node_type {
281            NodeType::Join => Ok(node),
282            NodeType::True | NodeType::Data | NodeType::PairList => {
283                Err(IntegrityError::InvalidStructure)
284            },
285        }
286    }
287
288    fn decode_pair_list_entry(
289        &self,
290        tag: Tag,
291        pairs: &[(u32, u32)],
292    ) -> Result<Node, IntegrityError> {
293        let node_type = self
294            .precompiles
295            .decode_node_type(tag)
296            .map_err(|_| IntegrityError::InvalidStructure)?;
297        let NodeType::PairList = node_type else {
298            return Err(IntegrityError::InvalidStructure);
299        };
300
301        let pairs = pairs
302            .iter()
303            .map(|(lhs, rhs)| Ok((self.resolve_index(*lhs)?, self.resolve_index(*rhs)?)))
304            .collect::<Result<Vec<_>, IntegrityError>>()?;
305        let node = Node::try_pair_list(tag, pairs).map_err(|_| IntegrityError::InvalidStructure)?;
306        node_type.validate_node(&node).map_err(|_| IntegrityError::InvalidStructure)?;
307        Ok(node)
308    }
309
310    fn resolve_index(&self, idx: u32) -> Result<Digest, IntegrityError> {
311        self.index_to_digest
312            .get(idx as usize)
313            .copied()
314            .ok_or(IntegrityError::InvalidStructure)
315    }
316
317    fn push_entry(&mut self, node: Node) -> Result<(), IntegrityError> {
318        if node.is_true() {
319            return Err(IntegrityError::InvalidStructure);
320        }
321
322        let digest = node.digest();
323        if !self.seen_digests.insert(digest) {
324            return Err(IntegrityError::InvalidStructure);
325        }
326
327        let index = self.index_to_digest.len();
328        if index > u32::MAX as usize {
329            return Err(IntegrityError::InvalidStructure);
330        }
331
332        self.entries.push((digest, node));
333        self.index_to_digest.push(digest);
334        Ok(())
335    }
336}
337
338// WIRE ENCODING
339// ================================================================================================
340
341/// Encoder for the canonical topological wire format used by [`super::DeferredState::to_wire`].
342#[derive(Default)]
343struct WireEncoder {
344    seen: BTreeSet<Digest>,
345    by_digest: BTreeMap<Digest, u32>,
346    entries: Vec<WireEntry>,
347}
348
349impl WireEncoder {
350    fn visit_state_digest(
351        &mut self,
352        state: &DeferredState,
353        digest: Digest,
354    ) -> Result<(), IntegrityError> {
355        let mut pending = Vec::new();
356        pending.push(WireEncodeStep::Visit(digest));
357
358        while let Some(step) = pending.pop() {
359            match step {
360                WireEncodeStep::Visit(digest) => {
361                    self.schedule_digest(state, digest, &mut pending)?
362                },
363                WireEncodeStep::Emit(digest) => {
364                    let entry = self.entry_for_digest(state, digest)?;
365                    self.push_entry(digest, entry)?;
366                },
367            }
368        }
369
370        Ok(())
371    }
372
373    fn schedule_digest(
374        &mut self,
375        state: &DeferredState,
376        digest: Digest,
377        pending: &mut Vec<WireEncodeStep>,
378    ) -> Result<(), IntegrityError> {
379        if digest == TRUE_DIGEST || !self.seen.insert(digest) {
380            return Ok(());
381        }
382
383        let node = self.validated_node(state, digest)?;
384        pending.push(WireEncodeStep::Emit(digest));
385
386        match self.node_type(state, node)? {
387            NodeType::Data => {},
388            NodeType::Join => {
389                let (lhs, rhs) =
390                    node.payload().as_join().map_err(|_| IntegrityError::InvalidStructure)?;
391                pending.push(WireEncodeStep::Visit(rhs));
392                pending.push(WireEncodeStep::Visit(lhs));
393            },
394            NodeType::PairList => {
395                let pairs =
396                    node.payload().as_pair_list().map_err(|_| IntegrityError::InvalidStructure)?;
397                for (lhs, rhs) in pairs.iter().rev() {
398                    pending.push(WireEncodeStep::Visit(*rhs));
399                    pending.push(WireEncodeStep::Visit(*lhs));
400                }
401            },
402            NodeType::True => return Err(IntegrityError::InvalidStructure),
403        };
404
405        Ok(())
406    }
407
408    fn entry_for_digest(
409        &self,
410        state: &DeferredState,
411        digest: Digest,
412    ) -> Result<WireEntry, IntegrityError> {
413        let node = self.validated_node(state, digest)?;
414
415        Ok(match self.node_type(state, node)? {
416            NodeType::Data => WireEntry::Data {
417                tag: node.tag(),
418                chunks: node
419                    .payload()
420                    .as_data()
421                    .map_err(|_| IntegrityError::InvalidStructure)?
422                    .to_vec(),
423            },
424            NodeType::Join => {
425                let (lhs, rhs) =
426                    node.payload().as_join().map_err(|_| IntegrityError::InvalidStructure)?;
427                let lhs = self.index_for(lhs)?;
428                let rhs = self.index_for(rhs)?;
429                WireEntry::Join { tag: node.tag(), lhs, rhs }
430            },
431            NodeType::PairList => {
432                let pairs =
433                    node.payload().as_pair_list().map_err(|_| IntegrityError::InvalidStructure)?;
434                let pairs = pairs
435                    .iter()
436                    .map(|(lhs, rhs)| Ok((self.index_for(*lhs)?, self.index_for(*rhs)?)))
437                    .collect::<Result<Vec<_>, IntegrityError>>()?;
438                WireEntry::PairList { tag: node.tag(), pairs }
439            },
440            NodeType::True => return Err(IntegrityError::InvalidStructure),
441        })
442    }
443
444    fn validated_node<'a>(
445        &self,
446        state: &'a DeferredState,
447        digest: Digest,
448    ) -> Result<&'a Node, IntegrityError> {
449        let node = state.get_node(&digest).ok_or(IntegrityError::InvalidStructure)?;
450        self.node_type(state, node)?
451            .validate_node(node)
452            .map_err(|_| IntegrityError::InvalidStructure)?;
453        Ok(node)
454    }
455
456    fn node_type(&self, state: &DeferredState, node: &Node) -> Result<NodeType, IntegrityError> {
457        state
458            .registry()
459            .decode_node_type(node.tag())
460            .map_err(|_| IntegrityError::InvalidStructure)
461    }
462
463    fn index_for(&self, digest: Digest) -> Result<u32, IntegrityError> {
464        if digest == TRUE_DIGEST {
465            return Ok(TRUE_INDEX);
466        }
467        self.by_digest.get(&digest).copied().ok_or(IntegrityError::InvalidStructure)
468    }
469
470    fn push_entry(&mut self, digest: Digest, entry: WireEntry) -> Result<(), IntegrityError> {
471        let next_index =
472            self.entries.len().checked_add(1).ok_or(IntegrityError::InvalidStructure)?;
473        let next_index = u32::try_from(next_index).map_err(|_| IntegrityError::InvalidStructure)?;
474        self.entries.push(entry);
475        self.by_digest.insert(digest, next_index);
476        Ok(())
477    }
478}
479
480enum WireEncodeStep {
481    Visit(Digest),
482    Emit(Digest),
483}
484
485// SERIALIZATION
486// ================================================================================================
487
488impl Serializable for WireEntry {
489    fn write_into<W: ByteWriter>(&self, target: &mut W) {
490        match self {
491            Self::Data { tag, chunks } => {
492                target.write_u8(0);
493                tag.write_into(target);
494                target.write_usize(chunks.len());
495                for chunk in chunks {
496                    for felt in chunk {
497                        felt.write_into(target);
498                    }
499                }
500            },
501            Self::Join { tag, lhs, rhs } => {
502                target.write_u8(1);
503                tag.write_into(target);
504                target.write_u32(*lhs);
505                target.write_u32(*rhs);
506            },
507            Self::PairList { tag, pairs } => {
508                target.write_u8(2);
509                tag.write_into(target);
510                target.write_usize(pairs.len());
511                for (lhs, rhs) in pairs {
512                    target.write_u32(*lhs);
513                    target.write_u32(*rhs);
514                }
515            },
516        }
517    }
518}
519
520impl Deserializable for WireEntry {
521    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
522        let mut remaining_elements = MAX_DEFERRED_ELEMENTS;
523        read_wire_entry(source, &mut remaining_elements)
524    }
525
526    fn min_serialized_size() -> usize {
527        1
528    }
529}
530
531struct WirePair((u32, u32));
532
533impl Deserializable for WirePair {
534    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
535        Ok(Self((source.read_u32()?, source.read_u32()?)))
536    }
537
538    fn min_serialized_size() -> usize {
539        u32::min_serialized_size() * 2
540    }
541}
542
543struct WireDataChunk(DataChunk);
544
545impl Deserializable for WireDataChunk {
546    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
547        let mut chunk = [ZERO; Node::DATA_CHUNK_FELT_LEN];
548        for felt in &mut chunk {
549            *felt = Felt::read_from(source)?;
550        }
551        Ok(Self(chunk))
552    }
553
554    fn min_serialized_size() -> usize {
555        Node::DATA_CHUNK_FELT_LEN * Felt::min_serialized_size()
556    }
557}
558
559impl Serializable for DeferredStateWire {
560    fn write_into<W: ByteWriter>(&self, target: &mut W) {
561        target.write_usize(self.entries.len());
562        for entry in &self.entries {
563            entry.write_into(target);
564        }
565    }
566}
567
568impl Deserializable for DeferredStateWire {
569    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
570        let entry_count = source.read_usize()?;
571        if entry_count > MAX_WIRE_ENTRIES {
572            return Err(DeserializationError::InvalidValue(format!(
573                "deferred wire contains {entry_count} entries, maximum is {MAX_WIRE_ENTRIES}"
574            )));
575        }
576
577        let mut remaining_elements = MAX_DEFERRED_ELEMENTS;
578        let mut entries = Vec::with_capacity(entry_count);
579        for _ in 0..entry_count {
580            entries.push(read_wire_entry(source, &mut remaining_elements)?);
581        }
582        Ok(Self { entries })
583    }
584
585    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
586        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
587        Self::read_from(&mut reader)
588    }
589
590    fn min_serialized_size() -> usize {
591        usize::min_serialized_size()
592    }
593}
594
595fn read_wire_entry<R: ByteReader>(
596    source: &mut R,
597    remaining_elements: &mut usize,
598) -> Result<WireEntry, DeserializationError> {
599    let discriminant = source.read_u8()?;
600    match discriminant {
601        0 => {
602            reserve_wire_elements(remaining_elements, Tag::FELT_LEN)?;
603            let tag = Tag::read_from(source)?;
604            let chunk_count = source.read_usize()?;
605            reserve_wire_payload(remaining_elements, chunk_count)?;
606            let chunks = source
607                .read_many_iter::<WireDataChunk>(chunk_count)?
608                .map(|chunk| chunk.map(|chunk| chunk.0))
609                .collect::<Result<_, _>>()?;
610            Ok(WireEntry::Data { tag, chunks })
611        },
612        1 => {
613            reserve_wire_elements(remaining_elements, Tag::FELT_LEN + Node::DATA_CHUNK_FELT_LEN)?;
614            let tag = Tag::read_from(source)?;
615            let lhs = source.read_u32()?;
616            let rhs = source.read_u32()?;
617            Ok(WireEntry::Join { tag, lhs, rhs })
618        },
619        2 => {
620            reserve_wire_elements(remaining_elements, Tag::FELT_LEN)?;
621            let tag = Tag::read_from(source)?;
622            let pair_count = source.read_usize()?;
623            reserve_wire_payload(remaining_elements, pair_count)?;
624            let pairs = source
625                .read_many_iter::<WirePair>(pair_count)?
626                .map(|pair| pair.map(|pair| pair.0))
627                .collect::<Result<_, _>>()?;
628            Ok(WireEntry::PairList { tag, pairs })
629        },
630        other => Err(DeserializationError::InvalidValue(format!(
631            "invalid deferred wire entry discriminant: {other}"
632        ))),
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use alloc::vec::Vec;
639
640    use super::*;
641    use crate::{
642        Felt,
643        deferred::{DeferredContext, Payload, Precompile, precompile_id},
644        serde::{ByteWriter, Serializable},
645    };
646
647    #[derive(Debug, Clone, Copy)]
648    struct PairListFixture;
649
650    impl PairListFixture {
651        const NAME: &'static str = "wire-pair-list-fixture";
652
653        fn tag() -> Tag {
654            Tag::precompile(precompile_id(Self::NAME), [ZERO; 3])
655                .expect("fixture id is precompile-owned")
656        }
657    }
658
659    impl Precompile for PairListFixture {
660        fn name(&self) -> &'static str {
661            Self::NAME
662        }
663
664        fn id(&self) -> Felt {
665            precompile_id(Self::NAME)
666        }
667
668        fn decode(&self, args: [Felt; 3]) -> Option<NodeType> {
669            (args == [ZERO; 3]).then_some(NodeType::PairList)
670        }
671
672        fn evaluate(
673            &self,
674            _args: [Felt; 3],
675            _payload: &Payload,
676            _context: &mut DeferredContext<'_>,
677        ) -> Result<Node, PrecompileError> {
678            Ok(Node::TRUE)
679        }
680    }
681
682    fn felts(seed: u64) -> [Felt; 8] {
683        core::array::from_fn(|i| Felt::new_unchecked(seed + i as u64))
684    }
685
686    fn tag(seed: u64) -> Tag {
687        Tag::from_word(felts(seed)[..4].try_into().unwrap())
688    }
689
690    fn wire(entries: Vec<WireEntry>) -> DeferredStateWire {
691        DeferredStateWire { entries }
692    }
693
694    fn assert_wire_round_trips(wire: DeferredStateWire) {
695        let decoded = DeferredStateWire::read_from_bytes(&wire.to_bytes()).unwrap();
696        assert_eq!(decoded, wire);
697    }
698
699    #[test]
700    fn empty_wire_vector_decodes_with_exact_canonical_budget() {
701        let wires = alloc::vec![DeferredStateWire::default(), DeferredStateWire::default()];
702        let bytes = wires.to_bytes();
703
704        assert_eq!(DeferredStateWire::min_serialized_size(), 1);
705        assert_eq!(bytes.len(), 3);
706        assert_eq!(
707            Vec::<DeferredStateWire>::read_from_bytes_with_budget(&bytes, bytes.len()).unwrap(),
708            wires
709        );
710    }
711
712    #[test]
713    fn wire_decoder_accepts_exact_framework_chunks_data() {
714        let registry = PrecompileRegistry::new();
715        let chunks = alloc::vec![felts(10), felts(20)];
716        let wire = wire(alloc::vec![WireEntry::Data { tag: Tag::CHUNKS, chunks: chunks.clone() }]);
717        let node = Node::chunks(chunks).unwrap();
718
719        let (entries, root) = WireDecoder::new(&wire, &registry).unwrap().decode().unwrap();
720
721        assert_eq!(entries, alloc::vec![(node.digest(), node.clone())]);
722        assert_eq!(root, node.digest());
723    }
724
725    #[test]
726    fn empty_wire_rehydrates_with_the_fixed_deferred_element_limit() {
727        let state = DeferredState::from_wire(
728            Arc::new(PrecompileRegistry::new()),
729            &DeferredStateWire::default(),
730        )
731        .unwrap();
732
733        assert_eq!(state.num_elements(), 0);
734        assert_eq!(state.remaining_elements(), MAX_DEFERRED_ELEMENTS);
735    }
736
737    #[test]
738    fn in_memory_wire_element_limit_accepts_exact_and_rejects_next_entry() {
739        let join = WireEntry::Join {
740            tag: Tag::AND,
741            lhs: TRUE_INDEX,
742            rhs: TRUE_INDEX,
743        };
744        let join_elements = Tag::FELT_LEN + Node::DATA_CHUNK_FELT_LEN;
745        let join_count = MAX_DEFERRED_ELEMENTS / join_elements;
746        let mut entries = alloc::vec![join; join_count];
747        entries.push(WireEntry::Data { tag: Tag::CHUNKS, chunks: Vec::new() });
748        let mut wire = DeferredStateWire { entries };
749
750        wire.validate_element_limit().unwrap();
751
752        wire.entries.push(WireEntry::Data { tag: Tag::CHUNKS, chunks: Vec::new() });
753        assert!(matches!(
754            wire.validate_element_limit(),
755            Err(IntegrityError::DeferredStateTooLarge { .. })
756        ));
757    }
758
759    #[test]
760    fn rehydration_rejects_empty_data_and_pair_list_entries() {
761        let empty_data =
762            wire(alloc::vec![WireEntry::Data { tag: Tag::CHUNKS, chunks: Vec::new() }]);
763        assert!(matches!(
764            DeferredState::from_wire(Arc::new(PrecompileRegistry::new()), &empty_data),
765            Err(IntegrityError::InvalidStructure)
766        ));
767
768        let empty_pairs = wire(alloc::vec![WireEntry::PairList {
769            tag: PairListFixture::tag(),
770            pairs: Vec::new(),
771        }]);
772        assert!(matches!(
773            DeferredState::from_wire(
774                Arc::new(PrecompileRegistry::new().with_precompile(PairListFixture)),
775                &empty_pairs,
776            ),
777            Err(IntegrityError::InvalidStructure)
778        ));
779    }
780
781    #[test]
782    fn wire_decoder_rejects_malformed_framework_chunks_data() {
783        let registry = PrecompileRegistry::new();
784        let malformed = Tag::from_word([Tag::CHUNKS.id(), Felt::new_unchecked(1), ZERO, ZERO]);
785        let wire = wire(alloc::vec![WireEntry::Data {
786            tag: malformed,
787            chunks: alloc::vec![felts(10)],
788        }]);
789
790        assert!(matches!(
791            WireDecoder::new(&wire, &registry).unwrap().decode(),
792            Err(IntegrityError::InvalidStructure)
793        ));
794    }
795
796    /// The proof-transit format must round-trip every entry variant and the empty root opening.
797    #[test]
798    fn wire_serialize_round_trip_all_entries() {
799        assert_wire_round_trips(wire(alloc::vec![
800            WireEntry::Data {
801                tag: tag(1),
802                chunks: alloc::vec![felts(10)]
803            },
804            WireEntry::Data {
805                tag: tag(2),
806                chunks: alloc::vec![felts(20), felts(30)],
807            },
808            WireEntry::Join { tag: tag(3), lhs: 1, rhs: TRUE_INDEX },
809            WireEntry::PairList {
810                tag: tag(5),
811                pairs: alloc::vec![(1, 2), (TRUE_INDEX, 3)],
812            },
813        ]));
814        assert_wire_round_trips(DeferredStateWire::default());
815    }
816
817    #[test]
818    fn wire_encoder_omits_nodes_unreachable_from_root() {
819        let mut state = DeferredState::default();
820        let orphan = state.register(Node::chunks(alloc::vec![[ZERO; 8]]).unwrap()).unwrap();
821        state.log_statement(TRUE_DIGEST).unwrap();
822
823        assert!(state.get_node(&orphan).is_some());
824        assert_eq!(
825            state.to_wire().unwrap().entries,
826            alloc::vec![WireEntry::Join {
827                tag: Tag::AND,
828                lhs: TRUE_INDEX,
829                rhs: TRUE_INDEX,
830            }]
831        );
832    }
833
834    #[test]
835    fn wire_encoder_handles_deep_roots_iteratively() {
836        let mut state = DeferredState::default();
837        for _ in 0..4_096 {
838            state.log_statement(TRUE_DIGEST).unwrap();
839        }
840
841        let root = state.root();
842        let wire = state.to_wire().unwrap();
843
844        assert_eq!(wire.entries.len(), 4_096);
845        assert_eq!(
846            wire.entries.last(),
847            Some(&WireEntry::Join {
848                tag: Tag::AND,
849                lhs: 4_095,
850                rhs: TRUE_INDEX,
851            })
852        );
853        assert_eq!(
854            DeferredState::from_wire(Arc::new(PrecompileRegistry::new()), &wire)
855                .unwrap()
856                .root(),
857            root
858        );
859    }
860
861    fn encoded_entry_count(entry_count: usize) -> Vec<u8> {
862        let mut bytes = Vec::new();
863        bytes.write_usize(entry_count);
864        bytes
865    }
866
867    #[test]
868    fn wire_rejects_over_budget_entry_count() {
869        assert!(
870            DeferredStateWire::read_from_bytes(&encoded_entry_count(MAX_WIRE_ENTRIES + 1)).is_err()
871        );
872    }
873
874    #[test]
875    fn wire_element_budget_accepts_exact_limit_and_rejects_one_more() {
876        let mut exact = MAX_DEFERRED_ELEMENTS;
877        reserve_wire_elements(&mut exact, MAX_DEFERRED_ELEMENTS).unwrap();
878        assert_eq!(exact, 0);
879
880        let mut oversized = MAX_DEFERRED_ELEMENTS;
881        assert!(reserve_wire_elements(&mut oversized, MAX_DEFERRED_ELEMENTS + 1).is_err());
882    }
883
884    #[test]
885    fn wire_rejects_over_budget_data_chunk_count() {
886        let mut bytes = Vec::new();
887        bytes.write_usize(1);
888        bytes.write_u8(0); // Data entry discriminant
889        tag(1).write_into(&mut bytes);
890        bytes.write_usize(usize::MAX);
891
892        assert!(DeferredStateWire::read_from_bytes(&bytes).is_err());
893    }
894
895    #[test]
896    fn wire_rejects_over_budget_pair_count() {
897        let mut bytes = Vec::new();
898        bytes.write_usize(1);
899        bytes.write_u8(2); // PairList entry discriminant
900        tag(1).write_into(&mut bytes);
901        bytes.write_usize(usize::MAX);
902
903        assert!(DeferredStateWire::read_from_bytes(&bytes).is_err());
904    }
905}