Skip to main content

miden_core/
proof.rs

1use alloc::{
2    collections::BTreeSet,
3    string::{String, ToString},
4    vec::Vec,
5};
6
7#[cfg(feature = "arbitrary")]
8use proptest::prelude::*;
9
10use crate::{
11    Word,
12    crypto::hash::Blake3_256,
13    deferred::{DeferredRoot, MAX_PRECOMPILE_ROOTS, PrecompileWitness, fold_deferred_root},
14    serde::{
15        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
16        SliceReader,
17    },
18};
19
20// CONSTANTS
21// ================================================================================================
22
23/// Hard encoded-size and per-allocation safety ceiling for every STARK proof.
24pub const MAX_STARK_PROOF_BYTES: usize = 64 * 1024 * 1024;
25
26const DEFERRED_PROOF_DISCRIMINANT: u8 = 0;
27const COMPLETE_PROOF_DISCRIMINANT: u8 = 1;
28
29/// The recursive VM verifier root declared by proofs from the current prover.
30pub const CURRENT_VM_VERIFIER_ROOT: Word = Word::new([
31    crate::Felt::new_unchecked(6531317477902291971),
32    crate::Felt::new_unchecked(6330145662319840937),
33    crate::Felt::new_unchecked(1346989877885786889),
34    crate::Felt::new_unchecked(2977526316393511162),
35]);
36/// The recursive precompile verifier root declared by proofs from the current prover.
37pub const CURRENT_PVM_VERIFIER_ROOT: Word = Word::new([
38    crate::Felt::new_unchecked(6808264455915748692),
39    crate::Felt::new_unchecked(7335842088828173809),
40    crate::Felt::new_unchecked(8794786252055803605),
41    crate::Felt::new_unchecked(8801448297059648773),
42]);
43
44// HASH FUNCTION
45// ================================================================================================
46
47/// A hash function used during STARK proof generation.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[cfg_attr(
50    all(feature = "arbitrary", test),
51    miden_test_serialization_macros::serialization_test
52)]
53#[repr(u8)]
54pub enum HashFunction {
55    /// BLAKE3 hash function with 256-bit output.
56    Blake3_256 = 0x01,
57    // IDs 0x02..=0x04 belonged to removed hash functions and must not be reused.
58    /// Keccak hash function with 256-bit output.
59    Keccak = 0x05,
60    /// Eidos hash function with 252-bit packed output.
61    Eidos = 0x06,
62}
63
64impl HashFunction {
65    /// Returns the collision resistance level (in bits) of this hash function.
66    pub const fn collision_resistance(&self) -> u32 {
67        match self {
68            HashFunction::Blake3_256 => Blake3_256::COLLISION_RESISTANCE,
69            HashFunction::Keccak => 128,
70            HashFunction::Eidos => 126,
71        }
72    }
73}
74
75/// Error type for invalid hash function strings.
76#[derive(Debug, thiserror::Error)]
77#[error("invalid hash function '{hash_function}'. Valid options are: blake3-256, keccak, eidos")]
78pub struct InvalidHashFunctionError {
79    pub hash_function: String,
80}
81
82impl TryFrom<u8> for HashFunction {
83    type Error = DeserializationError;
84
85    fn try_from(repr: u8) -> Result<Self, Self::Error> {
86        match repr {
87            0x01 => Ok(Self::Blake3_256),
88            0x05 => Ok(Self::Keccak),
89            0x06 => Ok(Self::Eidos),
90            _ => Err(DeserializationError::InvalidValue(format!(
91                "the hash function representation {repr} is not valid!"
92            ))),
93        }
94    }
95}
96
97impl TryFrom<&str> for HashFunction {
98    type Error = InvalidHashFunctionError;
99
100    fn try_from(hash_fn_str: &str) -> Result<Self, Self::Error> {
101        match hash_fn_str {
102            "blake3-256" => Ok(Self::Blake3_256),
103            "keccak" => Ok(Self::Keccak),
104            "eidos" => Ok(Self::Eidos),
105            _ => Err(InvalidHashFunctionError { hash_function: hash_fn_str.to_string() }),
106        }
107    }
108}
109
110#[cfg(feature = "arbitrary")]
111impl Arbitrary for HashFunction {
112    type Parameters = ();
113    type Strategy = BoxedStrategy<Self>;
114
115    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
116        any::<u8>()
117            .prop_map(|tag| match tag % 3 {
118                0 => Self::Blake3_256,
119                1 => Self::Keccak,
120                _ => Self::Eidos,
121            })
122            .boxed()
123    }
124}
125
126impl Serializable for HashFunction {
127    fn write_into<W: ByteWriter>(&self, target: &mut W) {
128        target.write_u8(*self as u8);
129    }
130}
131
132impl Deserializable for HashFunction {
133    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
134        source.read_u8()?.try_into()
135    }
136}
137
138// PROOF ARTIFACTS
139// ================================================================================================
140
141/// A Miden VM STARK proof together with its authenticated precompile obligation.
142#[derive(Debug, Clone, PartialEq, Eq)]
143#[cfg_attr(
144    all(feature = "arbitrary", test),
145    miden_test_serialization_macros::serialization_test
146)]
147pub struct VmProof {
148    pub proof: StarkProof,
149    pub precompile_root: DeferredRoot,
150}
151
152impl Serializable for VmProof {
153    fn write_into<W: ByteWriter>(&self, target: &mut W) {
154        self.proof.write_into(target);
155        self.precompile_root.write_into(target);
156    }
157}
158
159impl Deserializable for VmProof {
160    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
161        let proof = StarkProof::read_from(source)?;
162        let precompile_root = DeferredRoot::read_from(source)?;
163        Ok(Self { proof, precompile_root })
164    }
165
166    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
167        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
168        let proof = Self::read_from(&mut reader)?;
169        if reader.has_more_bytes() {
170            return Err(DeserializationError::InvalidValue(
171                "extra bytes after VM proof payload".into(),
172            ));
173        }
174        Ok(proof)
175    }
176
177    fn min_serialized_size() -> usize {
178        StarkProof::min_serialized_size() + DeferredRoot::min_serialized_size()
179    }
180}
181
182/// A precompile STARK proof with its ordered constituent roots.
183///
184/// Binary decoding enforces the fixed root ceiling before reserving root storage, but otherwise
185/// preserves the encoded artifact shape.
186#[derive(Debug, Clone, PartialEq, Eq)]
187#[cfg_attr(
188    all(feature = "arbitrary", test),
189    miden_test_serialization_macros::serialization_test
190)]
191pub struct PrecompileProof {
192    pub proof: StarkProof,
193    pub roots: Vec<DeferredRoot>,
194}
195
196impl PrecompileProof {
197    /// Computes the aggregate deferred root.
198    ///
199    /// For roots `[A, B, C]`, this hashes `AND(A, B)`, then `AND(result, C)`. Order and
200    /// duplicates matter. A single root is returned unchanged; an empty list returns `None`.
201    /// This method does not verify the proof.
202    pub fn aggregate_root(&self) -> Option<DeferredRoot> {
203        self.roots.iter().copied().reduce(fold_deferred_root)
204    }
205}
206
207impl Serializable for PrecompileProof {
208    fn write_into<W: ByteWriter>(&self, target: &mut W) {
209        self.proof.write_into(target);
210        self.roots.write_into(target);
211    }
212}
213
214impl Deserializable for PrecompileProof {
215    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
216        let proof = StarkProof::read_from(source)?;
217        let root_count = source.read_usize()?;
218        if root_count > MAX_PRECOMPILE_ROOTS {
219            return Err(DeserializationError::InvalidValue(format!(
220                "precompile proof contains too many roots: found {root_count}, maximum is {MAX_PRECOMPILE_ROOTS}"
221            )));
222        }
223        let roots = source
224            .read_many_iter::<DeferredRoot>(root_count)?
225            .collect::<Result<Vec<_>, _>>()?;
226        Ok(Self { proof, roots })
227    }
228
229    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
230        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
231        let proof = Self::read_from(&mut reader)?;
232        if reader.has_more_bytes() {
233            return Err(DeserializationError::InvalidValue(
234                "extra bytes after precompile proof payload".into(),
235            ));
236        }
237        Ok(proof)
238    }
239
240    fn min_serialized_size() -> usize {
241        StarkProof::min_serialized_size() + usize::min_serialized_size()
242    }
243}
244
245/// The transport format and recursive verifier roots declared by an execution proof.
246///
247/// Verifier roots are listed in chronological order, with the newest root last. Root order does
248/// not affect compatibility checks.
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct ExecutionProofCompatibility {
251    format: u8,
252    vm_verifier_roots: Vec<Word>,
253    pvm_verifier_roots: Vec<Word>,
254}
255
256impl ExecutionProofCompatibility {
257    /// Execution proof transport format carrying portable singleton witnesses.
258    pub const FORMAT_V2: u8 = 2;
259
260    /// Creates a proof compatibility declaration for format `2`.
261    ///
262    /// # Errors
263    ///
264    /// Returns an error if either verifier root list contains duplicates.
265    pub fn new(
266        vm_verifier_roots: Vec<Word>,
267        pvm_verifier_roots: Vec<Word>,
268    ) -> Result<Self, ExecutionProofCompatibilityError> {
269        if has_duplicate(&vm_verifier_roots) {
270            return Err(ExecutionProofCompatibilityError::DuplicateVmVerifierRoot);
271        }
272        if has_duplicate(&pvm_verifier_roots) {
273            return Err(ExecutionProofCompatibilityError::DuplicatePvmVerifierRoot);
274        }
275
276        Ok(Self {
277            format: Self::FORMAT_V2,
278            vm_verifier_roots,
279            pvm_verifier_roots,
280        })
281    }
282
283    /// Returns the compatibility declared by proofs produced by the current prover.
284    pub fn current() -> Self {
285        Self::new(alloc::vec![CURRENT_VM_VERIFIER_ROOT], alloc::vec![CURRENT_PVM_VERIFIER_ROOT])
286            .expect("current execution proof compatibility must not contain duplicate roots")
287    }
288
289    /// Returns the transport format.
290    pub const fn format(&self) -> u8 {
291        self.format
292    }
293
294    /// Returns the VM verifier roots which can be used to verify this proof.
295    ///
296    /// The roots are unique and listed in chronological order with the most recent verifier listed
297    /// last.
298    pub fn vm_verifier_roots(&self) -> &[Word] {
299        &self.vm_verifier_roots
300    }
301
302    /// Returns the precompile VM verifier roots which can be used to verify the precompile proof
303    /// associated with this execution proof, if any.
304    ///
305    /// The roots are unique and listed in chronological order with the most recent verifier listed
306    /// last.
307    pub fn pvm_verifier_roots(&self) -> &[Word] {
308        &self.pvm_verifier_roots
309    }
310}
311
312/// Errors returned while constructing execution proof compatibility metadata.
313#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
314pub enum ExecutionProofCompatibilityError {
315    /// The VM verifier root list contains a duplicate.
316    #[error("VM verifier roots must not contain duplicates")]
317    DuplicateVmVerifierRoot,
318    /// The precompile VM verifier root list contains a duplicate.
319    #[error("PVM verifier roots must not contain duplicates")]
320    DuplicatePvmVerifierRoot,
321}
322
323/// The state of precompile work associated with an execution proof.
324#[derive(Debug, Clone, PartialEq, Eq)]
325pub enum PrecompileStatus {
326    /// The VM made no precompile requests.
327    Empty,
328    /// The VM made precompile requests which have not been proven.
329    Deferred(PrecompileWitness),
330    /// The VM made precompile requests and their proof is available.
331    Proven(PrecompileProof),
332}
333
334/// A versioned Miden VM execution proof.
335///
336/// This type preserves proof artifacts without establishing their validity.
337#[derive(Debug, Clone, PartialEq, Eq)]
338#[cfg_attr(
339    all(feature = "arbitrary", test),
340    miden_test_serialization_macros::serialization_test
341)]
342pub struct ExecutionProof {
343    compatibility: ExecutionProofCompatibility,
344    vm: VmProof,
345    precompile: PrecompileStatus,
346}
347
348impl ExecutionProof {
349    /// Creates an execution proof with the compatibility declared by the current prover.
350    pub fn new(vm: VmProof, precompile: PrecompileStatus) -> Self {
351        Self::from_parts(ExecutionProofCompatibility::current(), vm, precompile)
352    }
353
354    /// Creates an execution proof from its compatibility, VM proof, and precompile state.
355    pub const fn from_parts(
356        compatibility: ExecutionProofCompatibility,
357        vm: VmProof,
358        precompile: PrecompileStatus,
359    ) -> Self {
360        Self { compatibility, vm, precompile }
361    }
362
363    /// Returns the compatibility declared by this proof.
364    pub const fn compatibility(&self) -> &ExecutionProofCompatibility {
365        &self.compatibility
366    }
367
368    /// Returns the VM proof.
369    pub const fn vm(&self) -> &VmProof {
370        &self.vm
371    }
372
373    /// Returns the precompile data carried by this proof.
374    ///
375    /// This status describes the attached data; it does not establish proof validity.
376    pub const fn precompile(&self) -> &PrecompileStatus {
377        &self.precompile
378    }
379
380    /// Splits this proof into its compatibility, VM proof, and precompile state.
381    pub fn into_parts(self) -> (ExecutionProofCompatibility, VmProof, PrecompileStatus) {
382        (self.compatibility, self.vm, self.precompile)
383    }
384
385    /// Returns whether this proof contains precompile work.
386    pub const fn has_precompiles(&self) -> bool {
387        !matches!(self.precompile, PrecompileStatus::Empty)
388    }
389
390    /// Transitions a deferred proof to complete by attaching a precompile proof.
391    ///
392    /// This method does not verify the precompile proof or check it against the VM proof. The
393    /// verifier performs those checks.
394    pub fn complete(mut self, precompile: PrecompileProof) -> Result<Self, ExecutionProofError> {
395        if !matches!(self.precompile, PrecompileStatus::Deferred(_)) {
396            return Err(ExecutionProofError::AlreadyComplete);
397        }
398        self.precompile = PrecompileStatus::Proven(precompile);
399        Ok(self)
400    }
401
402    /// Encodes this proof canonically.
403    pub fn to_bytes(&self) -> Vec<u8> {
404        Serializable::to_bytes(self)
405    }
406
407    /// Decodes a complete versioned proof from canonical bytes.
408    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
409        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
410        let proof = <Self as Deserializable>::read_from(&mut reader)?;
411
412        if reader.has_more_bytes() {
413            return Err(DeserializationError::InvalidValue(
414                "extra bytes after versioned proof payload".into(),
415            ));
416        }
417        if proof.to_bytes() != bytes {
418            return Err(DeserializationError::InvalidValue(
419                "versioned proof bytes are not canonically encoded".into(),
420            ));
421        }
422
423        Ok(proof)
424    }
425}
426
427impl Serializable for ExecutionProof {
428    fn write_into<W: ByteWriter>(&self, target: &mut W) {
429        target.write_u8(self.compatibility.format);
430        self.compatibility.vm_verifier_roots.write_into(target);
431        self.compatibility.pvm_verifier_roots.write_into(target);
432        self.write_into_v2(target);
433    }
434}
435
436impl Deserializable for ExecutionProof {
437    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
438        let format = source.read_u8()?;
439        if format != ExecutionProofCompatibility::FORMAT_V2 {
440            return Err(DeserializationError::InvalidValue(format!(
441                "unsupported execution proof format {format}"
442            )));
443        }
444
445        let vm_verifier_roots = Vec::<Word>::read_from(source)?;
446        let pvm_verifier_roots = Vec::<Word>::read_from(source)?;
447        let compatibility = ExecutionProofCompatibility::new(vm_verifier_roots, pvm_verifier_roots)
448            .map_err(|error| DeserializationError::InvalidValue(error.to_string()))?;
449
450        Self::read_from_v2(source, compatibility)
451    }
452
453    fn min_serialized_size() -> usize {
454        u8::min_serialized_size()
455            + Vec::<Word>::min_serialized_size()
456            + Vec::<Word>::min_serialized_size()
457            + ExecutionProof::min_serialized_size_v2()
458    }
459}
460
461impl ExecutionProof {
462    fn write_into_v2<W: ByteWriter>(&self, target: &mut W) {
463        match &self.precompile {
464            PrecompileStatus::Deferred(precompile) => {
465                target.write_u8(DEFERRED_PROOF_DISCRIMINANT);
466                self.vm.write_into(target);
467                precompile.write_into(target);
468            },
469            PrecompileStatus::Empty => {
470                target.write_u8(COMPLETE_PROOF_DISCRIMINANT);
471                self.vm.write_into(target);
472                Option::<PrecompileProof>::None.write_into(target);
473            },
474            PrecompileStatus::Proven(precompile) => {
475                target.write_u8(COMPLETE_PROOF_DISCRIMINANT);
476                self.vm.write_into(target);
477                Some(precompile).write_into(target);
478            },
479        }
480    }
481
482    fn read_from_v2<R: ByteReader>(
483        source: &mut R,
484        compatibility: ExecutionProofCompatibility,
485    ) -> Result<Self, DeserializationError> {
486        let discriminant = source.read_u8()?;
487        if !matches!(discriminant, DEFERRED_PROOF_DISCRIMINANT | COMPLETE_PROOF_DISCRIMINANT) {
488            return Err(DeserializationError::InvalidValue(format!(
489                "invalid execution proof discriminant {discriminant}"
490            )));
491        }
492
493        let vm = VmProof::read_from(source)?;
494        let precompile = match discriminant {
495            DEFERRED_PROOF_DISCRIMINANT => {
496                PrecompileStatus::Deferred(PrecompileWitness::read_from(source)?)
497            },
498            COMPLETE_PROOF_DISCRIMINANT => match Option::<PrecompileProof>::read_from(source)? {
499                Some(precompile) => PrecompileStatus::Proven(precompile),
500                None => PrecompileStatus::Empty,
501            },
502            _ => unreachable!("execution proof discriminant was checked before decoding"),
503        };
504
505        Ok(Self { compatibility, vm, precompile })
506    }
507
508    fn min_serialized_size_v2() -> usize {
509        u8::min_serialized_size()
510            + VmProof::min_serialized_size()
511            + Option::<PrecompileProof>::min_serialized_size()
512    }
513}
514
515/// Lifecycle errors returned while transitioning an execution proof.
516#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
517pub enum ExecutionProofError {
518    /// Only a deferred execution proof can be completed.
519    #[error("the execution proof is already complete")]
520    AlreadyComplete,
521}
522
523// STARK PROOF
524// ================================================================================================
525
526/// A serialized STARK proof and the hash function used during proof generation.
527#[derive(Debug, Clone, PartialEq, Eq)]
528#[cfg_attr(
529    all(feature = "arbitrary", test),
530    miden_test_serialization_macros::serialization_test
531)]
532pub struct StarkProof {
533    bytes: Vec<u8>,
534    hash_fn: HashFunction,
535}
536
537impl StarkProof {
538    /// Creates a new instance of [StarkProof] from proof bytes and hash function.
539    pub const fn new(bytes: Vec<u8>, hash_fn: HashFunction) -> Self {
540        Self { bytes, hash_fn }
541    }
542
543    /// Returns the serialized STARK proof bytes.
544    pub fn bytes(&self) -> &[u8] {
545        &self.bytes
546    }
547
548    /// Returns the hash function used during proof generation process.
549    pub const fn hash_fn(&self) -> HashFunction {
550        self.hash_fn
551    }
552
553    /// Returns the serialized STARK proof bytes and hash function.
554    pub fn into_parts(self) -> (Vec<u8>, HashFunction) {
555        (self.bytes, self.hash_fn)
556    }
557}
558
559impl Serializable for StarkProof {
560    fn write_into<W: ByteWriter>(&self, target: &mut W) {
561        self.bytes.write_into(target);
562        self.hash_fn.write_into(target);
563    }
564}
565
566impl Deserializable for StarkProof {
567    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
568        let byte_count = source.read_usize()?;
569        if byte_count > MAX_STARK_PROOF_BYTES {
570            return Err(DeserializationError::InvalidValue(format!(
571                "STARK proof contains too many bytes: found {byte_count}, maximum is {MAX_STARK_PROOF_BYTES}"
572            )));
573        }
574        let bytes = source.read_many_iter::<u8>(byte_count)?.collect::<Result<Vec<_>, _>>()?;
575        let hash_fn = HashFunction::read_from(source)?;
576        Ok(Self::new(bytes, hash_fn))
577    }
578
579    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
580        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
581        Self::read_from(&mut reader)
582    }
583
584    fn min_serialized_size() -> usize {
585        Vec::<u8>::min_serialized_size() + HashFunction::min_serialized_size()
586    }
587}
588
589// HELPER FUNCTIONS
590// ================================================================================================
591
592fn has_duplicate(roots: &[Word]) -> bool {
593    let mut unique = BTreeSet::new();
594    roots.iter().any(|root| !unique.insert(*root))
595}
596
597// TESTS
598// ================================================================================================
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use crate::{
604        Felt,
605        deferred::{DEFERRED_AND_FRAME, Node, PrecompileWitnessEntry, TRUE_DIGEST},
606        serde::ByteWriter,
607    };
608
609    fn dummy_stark_proof(bytes: &[u8]) -> StarkProof {
610        StarkProof::new(bytes.to_vec(), HashFunction::Eidos)
611    }
612
613    #[test]
614    fn hash_function_wire_tags_are_pinned_and_retired_tags_are_rejected() {
615        for (hash_fn, tag, name) in [
616            (HashFunction::Blake3_256, 0x01, "blake3-256"),
617            (HashFunction::Keccak, 0x05, "keccak"),
618            (HashFunction::Eidos, 0x06, "eidos"),
619        ] {
620            assert_eq!(hash_fn.to_bytes(), [tag]);
621            assert_eq!(HashFunction::read_from_bytes(&[tag]).unwrap(), hash_fn);
622            assert_eq!(HashFunction::try_from(name).unwrap(), hash_fn);
623        }
624        for (tag, name) in [(0x02, "rpo"), (0x03, "rpx"), (0x04, "poseidon2")] {
625            assert!(HashFunction::read_from_bytes(&[tag]).is_err());
626            assert!(HashFunction::try_from(name).is_err());
627        }
628    }
629
630    fn root(value: u64) -> DeferredRoot {
631        [Felt::new(value).unwrap(), Felt::ZERO, Felt::ZERO, Felt::ZERO].into()
632    }
633
634    fn vm_proof(precompile_root: DeferredRoot) -> VmProof {
635        VmProof {
636            proof: dummy_stark_proof(&[1]),
637            precompile_root,
638        }
639    }
640
641    fn precompile_proof(roots: &[DeferredRoot]) -> PrecompileProof {
642        PrecompileProof {
643            proof: dummy_stark_proof(&[2]),
644            roots: roots.to_vec(),
645        }
646    }
647
648    #[test]
649    fn aggregate_root_preserves_order_grouping_and_duplicates() {
650        let a = root(1);
651        let b = root(2);
652        let c = root(3);
653        let and = |lhs, rhs| Node::and(lhs, rhs).digest();
654
655        assert_eq!(precompile_proof(&[]).aggregate_root(), None);
656        assert_eq!(precompile_proof(&[a]).aggregate_root(), Some(a));
657        assert_eq!(precompile_proof(&[a, b, c]).aggregate_root(), Some(and(and(a, b), c)));
658        assert_ne!(precompile_proof(&[a, b, c]).aggregate_root(), Some(and(a, and(b, c))));
659        assert_ne!(
660            precompile_proof(&[a, b]).aggregate_root(),
661            precompile_proof(&[b, a]).aggregate_root()
662        );
663        assert_eq!(precompile_proof(&[a, b, a]).aggregate_root(), Some(and(and(a, b), a)));
664    }
665
666    fn wire() -> (PrecompileWitness, DeferredRoot) {
667        let witness = PrecompileWitness::from_entries(vec![PrecompileWitnessEntry::Join {
668            frame: DEFERRED_AND_FRAME,
669            lhs: 0,
670            rhs: 0,
671        }])
672        .unwrap();
673        let root = Node::and(TRUE_DIGEST, TRUE_DIGEST).digest();
674        (witness, root)
675    }
676
677    fn versioned(vm: VmProof, precompile: PrecompileStatus) -> ExecutionProof {
678        ExecutionProof::from_parts(
679            ExecutionProofCompatibility::new(vec![root(11), root(12)], vec![root(21)]).unwrap(),
680            vm,
681            precompile,
682        )
683    }
684
685    fn version_prefix() -> Vec<u8> {
686        let compatibility =
687            ExecutionProofCompatibility::new(vec![root(11), root(12)], vec![root(21)]).unwrap();
688        let mut bytes = vec![compatibility.format()];
689        compatibility.vm_verifier_roots().to_vec().write_into(&mut bytes);
690        compatibility.pvm_verifier_roots().to_vec().write_into(&mut bytes);
691        bytes
692    }
693
694    #[test]
695    fn execution_proof_reports_precompile_state() {
696        let (precompile_wire, wire_root) = wire();
697        let deferred = versioned(vm_proof(wire_root), PrecompileStatus::Deferred(precompile_wire));
698        assert!(deferred.has_precompiles());
699
700        let complete_without_precompile = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
701        assert!(!complete_without_precompile.has_precompiles());
702
703        let complete_with_precompile =
704            versioned(vm_proof(root(1)), PrecompileStatus::Proven(precompile_proof(&[root(1)])));
705        assert!(complete_with_precompile.has_precompiles());
706    }
707
708    #[test]
709    fn versioned_proof_round_trips_with_format_first() {
710        let proof = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
711
712        let bytes = proof.to_bytes();
713        let decoded = ExecutionProof::read_from_bytes(&bytes).unwrap();
714
715        assert_eq!(bytes[0], ExecutionProofCompatibility::FORMAT_V2);
716        assert_eq!(decoded, proof);
717        assert_eq!(decoded.compatibility().format(), ExecutionProofCompatibility::FORMAT_V2);
718        assert_eq!(decoded.compatibility().vm_verifier_roots(), &[root(11), root(12)]);
719        assert_eq!(decoded.compatibility().pvm_verifier_roots(), &[root(21)]);
720    }
721
722    #[test]
723    fn versioned_proof_decoder_rejects_unknown_format_before_body() {
724        for version in [0, 1, ExecutionProofCompatibility::FORMAT_V2 + 1] {
725            let error = ExecutionProof::read_from_bytes(&[version]).unwrap_err();
726            assert!(matches!(error, DeserializationError::InvalidValue(message)
727                if message.contains("unsupported execution proof format")));
728        }
729    }
730
731    #[test]
732    fn versioned_proof_decoder_rejects_trailing_and_noncanonical_bytes() {
733        let proof = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
734        let mut trailing = proof.to_bytes();
735        trailing.push(0);
736        assert!(ExecutionProof::read_from_bytes(&trailing).is_err());
737
738        let canonical = proof.to_bytes();
739        assert_eq!(canonical[1], 5, "two VM roots use a one-byte vint encoding");
740        let mut noncanonical = alloc::vec![canonical[0], 0];
741        noncanonical.extend_from_slice(&2u64.to_le_bytes());
742        noncanonical.extend_from_slice(&canonical[2..]);
743        let error = ExecutionProof::read_from_bytes(&noncanonical).unwrap_err();
744        assert!(
745            matches!(error, DeserializationError::InvalidValue(message) if message.contains("not canonically encoded"))
746        );
747    }
748
749    #[test]
750    fn versioned_proof_decoder_applies_the_input_budget_to_root_lists() {
751        let mut bytes = vec![ExecutionProofCompatibility::FORMAT_V2];
752        bytes.write_usize(usize::MAX);
753
754        let error = ExecutionProof::read_from_bytes(&bytes).unwrap_err();
755        assert!(matches!(error, DeserializationError::InvalidValue(_)));
756    }
757
758    #[test]
759    fn compatibility_constructor_rejects_duplicate_roots() {
760        assert_eq!(
761            ExecutionProofCompatibility::new(vec![root(1), root(1)], vec![]),
762            Err(ExecutionProofCompatibilityError::DuplicateVmVerifierRoot)
763        );
764        assert_eq!(
765            ExecutionProofCompatibility::new(vec![], vec![root(2), root(2)]),
766            Err(ExecutionProofCompatibilityError::DuplicatePvmVerifierRoot)
767        );
768    }
769
770    #[test]
771    fn versioned_proof_decoder_rejects_duplicate_roots() {
772        let proof = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
773        let proof_bytes = proof.to_bytes();
774        let body = &proof_bytes[version_prefix().len()..];
775
776        let mut duplicate_vm = vec![ExecutionProofCompatibility::FORMAT_V2];
777        vec![root(1), root(1)].write_into(&mut duplicate_vm);
778        Vec::<Word>::new().write_into(&mut duplicate_vm);
779        duplicate_vm.extend_from_slice(body);
780        let error = ExecutionProof::read_from_bytes(&duplicate_vm).unwrap_err();
781        assert!(
782            matches!(error, DeserializationError::InvalidValue(message) if message.contains("VM verifier roots must not contain duplicates"))
783        );
784
785        let mut duplicate_pvm = vec![ExecutionProofCompatibility::FORMAT_V2];
786        Vec::<Word>::new().write_into(&mut duplicate_pvm);
787        vec![root(2), root(2)].write_into(&mut duplicate_pvm);
788        duplicate_pvm.extend_from_slice(body);
789        let error = ExecutionProof::read_from_bytes(&duplicate_pvm).unwrap_err();
790        assert!(
791            matches!(error, DeserializationError::InvalidValue(message) if message.contains("PVM verifier roots must not contain duplicates"))
792        );
793    }
794
795    #[test]
796    fn proof_minimum_serialized_sizes_match_shortest_canonical_encodings() {
797        let stark = StarkProof::new(Vec::new(), HashFunction::Eidos);
798        assert_eq!(StarkProof::min_serialized_size(), stark.to_bytes().len());
799        assert_eq!(StarkProof::min_serialized_size(), 2);
800
801        let vm = VmProof {
802            proof: stark,
803            precompile_root: TRUE_DIGEST,
804        };
805        assert_eq!(VmProof::min_serialized_size(), vm.to_bytes().len());
806        assert_eq!(VmProof::min_serialized_size(), 34);
807
808        let empty = PrecompileProof {
809            proof: StarkProof::new(Vec::new(), HashFunction::Eidos),
810            roots: Vec::new(),
811        };
812        assert_eq!(PrecompileProof::min_serialized_size(), empty.to_bytes().len());
813        assert_eq!(PrecompileProof::min_serialized_size(), 3);
814
815        let singleton = PrecompileProof {
816            proof: StarkProof::new(Vec::new(), HashFunction::Eidos),
817            roots: alloc::vec![root(1)],
818        };
819        assert_eq!(singleton.to_bytes().len(), 35);
820
821        let proofs = alloc::vec![singleton.clone(), singleton];
822        let bytes = proofs.to_bytes();
823        assert_eq!(bytes.len(), 71);
824        let decoded = Vec::<PrecompileProof>::read_from_bytes_with_budget(&bytes, 71).unwrap();
825        assert_eq!(decoded.to_bytes(), bytes);
826    }
827
828    #[test]
829    fn stark_proof_decoder_rejects_oversized_length_before_payload() {
830        let mut bytes = Vec::new();
831        bytes.write_usize(MAX_STARK_PROOF_BYTES + 1);
832
833        let error = StarkProof::read_from_bytes(&bytes).unwrap_err();
834        let DeserializationError::InvalidValue(message) = error else {
835            panic!("expected excessive STARK proof length to be rejected")
836        };
837        assert!(message.contains("STARK proof contains too many bytes"));
838    }
839
840    #[test]
841    fn precompile_proof_decoder_rejects_oversized_root_count_before_payload() {
842        let at_limit = precompile_proof(&[root(3); MAX_PRECOMPILE_ROOTS]);
843        assert_eq!(PrecompileProof::read_from_bytes(&at_limit.to_bytes()).unwrap(), at_limit);
844        let mut bytes = dummy_stark_proof(&[2]).to_bytes();
845        bytes.write_usize(MAX_PRECOMPILE_ROOTS + 1);
846
847        let error = PrecompileProof::read_from_bytes(&bytes).unwrap_err();
848        let DeserializationError::InvalidValue(message) = error else {
849            panic!("expected excessive root count to be rejected")
850        };
851        assert!(message.contains("precompile proof contains too many roots"));
852    }
853
854    #[test]
855    fn standalone_proof_decoders_reject_trailing_bytes() {
856        let mut vm_bytes = vm_proof(root(3)).to_bytes();
857        vm_bytes.push(0);
858        assert!(VmProof::read_from_bytes(&vm_bytes).is_err());
859
860        let mut precompile_bytes = precompile_proof(&[root(3)]).to_bytes();
861        precompile_bytes.push(0);
862        assert!(PrecompileProof::read_from_bytes(&precompile_bytes).is_err());
863    }
864
865    #[test]
866    fn proof_artifacts_round_trip_canonically() {
867        let stark = dummy_stark_proof(&[1, 2, 3]);
868        let stark_bytes = stark.to_bytes();
869        let decoded_stark = StarkProof::read_from_bytes(&stark_bytes).unwrap();
870        assert_eq!(decoded_stark.to_bytes(), stark_bytes);
871
872        let vm = vm_proof(root(3));
873        let vm_bytes = vm.to_bytes();
874        let decoded_vm = VmProof::read_from_bytes(&vm_bytes).unwrap();
875        assert_eq!(decoded_vm.to_bytes(), vm_bytes);
876
877        let precompile = precompile_proof(&[]);
878        let precompile_bytes = precompile.to_bytes();
879        let decoded_precompile = PrecompileProof::read_from_bytes(&precompile_bytes).unwrap();
880        assert_eq!(decoded_precompile.to_bytes(), precompile_bytes);
881
882        let (precompile_wire, wire_root) = wire();
883        let proofs = [
884            versioned(vm_proof(wire_root), PrecompileStatus::Deferred(precompile_wire)),
885            versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty),
886            versioned(
887                vm_proof(TRUE_DIGEST),
888                PrecompileStatus::Proven(precompile_proof(&[TRUE_DIGEST])),
889            ),
890        ];
891        for proof in proofs {
892            let bytes = proof.to_bytes();
893            assert_eq!(ExecutionProof::read_from_bytes(&bytes).unwrap(), proof);
894        }
895    }
896
897    #[test]
898    fn complete_transitions_deferred_proof_without_validating_artifact_shape() {
899        let vm = vm_proof(TRUE_DIGEST);
900        let precompile = precompile_proof(&[]);
901        let deferred = versioned(vm.clone(), PrecompileStatus::Deferred(wire().0));
902
903        let completed = deferred.complete(precompile.clone()).unwrap();
904
905        let (completed_compatibility, completed_vm, completed_precompile) = completed.into_parts();
906        let PrecompileStatus::Proven(completed_precompile) = completed_precompile else {
907            panic!("deferred proof should transition to complete")
908        };
909        assert_eq!(
910            completed_compatibility,
911            ExecutionProofCompatibility::new(vec![root(11), root(12)], vec![root(21)]).unwrap()
912        );
913        assert_eq!(completed_vm.to_bytes(), vm.to_bytes());
914        assert_eq!(completed_precompile.to_bytes(), precompile.to_bytes());
915    }
916
917    #[test]
918    fn complete_rejects_an_already_complete_proof() {
919        let complete = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
920
921        assert!(matches!(
922            complete.complete(precompile_proof(&[])),
923            Err(ExecutionProofError::AlreadyComplete)
924        ));
925    }
926
927    #[test]
928    fn versioned_proof_transport_rejects_bad_discriminants_and_stark_bounds() {
929        let mut bad_discriminant = version_prefix();
930        bad_discriminant.write_u8(9);
931        assert!(ExecutionProof::read_from_bytes(&bad_discriminant).is_err());
932
933        let canonical = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty).to_bytes();
934        let prefix_len = version_prefix().len();
935        assert_eq!(canonical[prefix_len + 1], 3, "one STARK byte uses a one-byte vint encoding");
936        let mut noncanonical = canonical[..prefix_len + 1].to_vec();
937        noncanonical.push(0);
938        noncanonical.extend_from_slice(&1u64.to_le_bytes());
939        noncanonical.extend_from_slice(&canonical[prefix_len + 2..]);
940        let error = ExecutionProof::read_from_bytes(&noncanonical).unwrap_err();
941        assert!(
942            matches!(error, DeserializationError::InvalidValue(message) if message.contains("not canonically encoded"))
943        );
944
945        let mut oversized_proof = version_prefix();
946        oversized_proof.write_u8(COMPLETE_PROOF_DISCRIMINANT);
947        oversized_proof.write_usize(MAX_STARK_PROOF_BYTES + 1);
948        let error = ExecutionProof::read_from_bytes(&oversized_proof).unwrap_err();
949        assert!(
950            matches!(error, DeserializationError::InvalidValue(message) if message.contains("STARK proof contains too many bytes"))
951        );
952    }
953}
954
955// ARBITRARY (proptest)
956// ================================================================================================
957
958#[cfg(any(test, feature = "arbitrary"))]
959mod proof_arbitrary {
960    use proptest::prelude::*;
961
962    use super::{
963        ExecutionProof, ExecutionProofCompatibility, HashFunction, PrecompileProof,
964        PrecompileStatus, PrecompileWitness, StarkProof, VmProof,
965    };
966    use crate::{Felt, Word};
967
968    impl Arbitrary for StarkProof {
969        type Parameters = ();
970        type Strategy = BoxedStrategy<Self>;
971
972        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
973            // The reader enforces only the byte-count ceiling (64 MiB); bound well under it. The
974            // bytes are an opaque serialized artifact - the type does not validate their contents.
975            (
976                proptest::collection::vec(any::<u8>(), 0..=64),
977                prop_oneof![
978                    Just(HashFunction::Blake3_256),
979                    Just(HashFunction::Keccak),
980                    Just(HashFunction::Eidos),
981                ],
982            )
983                .prop_map(|(bytes, hash_fn)| StarkProof::new(bytes, hash_fn))
984                .boxed()
985        }
986    }
987
988    impl Arbitrary for VmProof {
989        type Parameters = ();
990        type Strategy = BoxedStrategy<Self>;
991
992        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
993            // DeferredRoot is a Word alias; both fields are any-valid.
994            (any::<StarkProof>(), any::<Word>())
995                .prop_map(|(proof, precompile_root)| Self { proof, precompile_root })
996                .boxed()
997        }
998    }
999
1000    impl Arbitrary for PrecompileProof {
1001        type Parameters = ();
1002        type Strategy = BoxedStrategy<Self>;
1003
1004        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1005            // The reader enforces the MAX_PRECOMPILE_ROOTS ceiling; bound well under it.
1006            (any::<StarkProof>(), proptest::collection::vec(any::<Word>(), 0..=4))
1007                .prop_map(|(proof, roots)| Self { proof, roots })
1008                .boxed()
1009        }
1010    }
1011
1012    impl Arbitrary for PrecompileStatus {
1013        type Parameters = ();
1014        type Strategy = BoxedStrategy<Self>;
1015
1016        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1017            prop_oneof![
1018                Just(Self::Empty),
1019                any::<PrecompileWitness>().prop_map(Self::Deferred).boxed(),
1020                any::<PrecompileProof>().prop_map(Self::Proven).boxed(),
1021            ]
1022            .boxed()
1023        }
1024    }
1025
1026    impl Arbitrary for ExecutionProof {
1027        type Parameters = ();
1028        type Strategy = BoxedStrategy<Self>;
1029
1030        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1031            // Sound by construction: compatibility roots are index-derived (distinct within each
1032            // list, satisfying the duplicate check); the rest composes covered parts.
1033            (0usize..=3, 0usize..=3, any::<VmProof>(), any::<PrecompileStatus>())
1034                .prop_map(|(vm_count, pvm_count, vm, precompile)| {
1035                    let compatibility = ExecutionProofCompatibility::new(
1036                        (0..vm_count).map(|i| [Felt::from(i as u32); 4].into()).collect(),
1037                        (0..pvm_count).map(|i| [Felt::from((i as u32) + 1000); 4].into()).collect(),
1038                    )
1039                    .expect("index-derived roots are distinct");
1040                    Self::from_parts(compatibility, vm, precompile)
1041                })
1042                .boxed()
1043        }
1044    }
1045}