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