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, DeferredStateWire, MAX_PRECOMPILE_ROOTS},
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(3472736072004736895),
32    crate::Felt::new_unchecked(9258997376938263475),
33    crate::Felt::new_unchecked(2749074330562194466),
34    crate::Felt::new_unchecked(16770332914073895013),
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(9567976034529193007),
39    crate::Felt::new_unchecked(16098679426107462596),
40    crate::Felt::new_unchecked(270852360285345822),
41    crate::Felt::new_unchecked(12812280715059172950),
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 Serializable for PrecompileProof {
202    fn write_into<W: ByteWriter>(&self, target: &mut W) {
203        self.proof.write_into(target);
204        self.roots.write_into(target);
205    }
206}
207
208impl Deserializable for PrecompileProof {
209    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
210        let proof = StarkProof::read_from(source)?;
211        let root_count = source.read_usize()?;
212        if root_count > MAX_PRECOMPILE_ROOTS {
213            return Err(DeserializationError::InvalidValue(format!(
214                "precompile proof contains too many roots: found {root_count}, maximum is {MAX_PRECOMPILE_ROOTS}"
215            )));
216        }
217        let roots = source
218            .read_many_iter::<DeferredRoot>(root_count)?
219            .collect::<Result<Vec<_>, _>>()?;
220        Ok(Self { proof, roots })
221    }
222
223    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
224        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
225        let proof = Self::read_from(&mut reader)?;
226        if reader.has_more_bytes() {
227            return Err(DeserializationError::InvalidValue(
228                "extra bytes after precompile proof payload".into(),
229            ));
230        }
231        Ok(proof)
232    }
233
234    fn min_serialized_size() -> usize {
235        StarkProof::min_serialized_size() + usize::min_serialized_size()
236    }
237}
238
239/// The transport format and recursive verifier roots declared by an execution proof.
240///
241/// Verifier roots are listed in chronological order, with the newest root last. Root order does
242/// not affect compatibility checks.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct ExecutionProofCompatibility {
245    format: u8,
246    vm_verifier_roots: Vec<Word>,
247    pvm_verifier_roots: Vec<Word>,
248}
249
250impl ExecutionProofCompatibility {
251    /// The first execution proof transport format.
252    pub const FORMAT_V1: u8 = 1;
253
254    /// Creates a proof compatibility declaration for format `1`.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if either verifier root list contains duplicates.
259    pub fn new(
260        vm_verifier_roots: Vec<Word>,
261        pvm_verifier_roots: Vec<Word>,
262    ) -> Result<Self, ExecutionProofCompatibilityError> {
263        if has_duplicate(&vm_verifier_roots) {
264            return Err(ExecutionProofCompatibilityError::DuplicateVmVerifierRoot);
265        }
266        if has_duplicate(&pvm_verifier_roots) {
267            return Err(ExecutionProofCompatibilityError::DuplicatePvmVerifierRoot);
268        }
269
270        Ok(Self {
271            format: Self::FORMAT_V1,
272            vm_verifier_roots,
273            pvm_verifier_roots,
274        })
275    }
276
277    /// Returns the compatibility declared by proofs produced by the current prover.
278    pub fn current() -> Self {
279        Self::new(alloc::vec![CURRENT_VM_VERIFIER_ROOT], alloc::vec![CURRENT_PVM_VERIFIER_ROOT])
280            .expect("current execution proof compatibility must not contain duplicate roots")
281    }
282
283    /// Returns the transport format.
284    pub const fn format(&self) -> u8 {
285        self.format
286    }
287
288    /// Returns the VM verifier roots which can be used to verify this proof.
289    ///
290    /// The roots are unique and listed in chronological order with the most recent verifier listed
291    /// last.
292    pub fn vm_verifier_roots(&self) -> &[Word] {
293        &self.vm_verifier_roots
294    }
295
296    /// Returns the precompile VM verifier roots which can be used to verify the precompile proof
297    /// associated with this execution proof, if any.
298    ///
299    /// The roots are unique and listed in chronological order with the most recent verifier listed
300    /// last.
301    pub fn pvm_verifier_roots(&self) -> &[Word] {
302        &self.pvm_verifier_roots
303    }
304}
305
306/// Errors returned while constructing execution proof compatibility metadata.
307#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
308pub enum ExecutionProofCompatibilityError {
309    /// The VM verifier root list contains a duplicate.
310    #[error("VM verifier roots must not contain duplicates")]
311    DuplicateVmVerifierRoot,
312    /// The precompile VM verifier root list contains a duplicate.
313    #[error("PVM verifier roots must not contain duplicates")]
314    DuplicatePvmVerifierRoot,
315}
316
317/// The state of precompile work associated with an execution proof.
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub enum PrecompileStatus {
320    /// The VM made no precompile requests.
321    Empty,
322    /// The VM made precompile requests which have not been proven.
323    Deferred(DeferredStateWire),
324    /// The VM made precompile requests and their proof is available.
325    Proven(PrecompileProof),
326}
327
328/// A versioned Miden VM execution proof.
329///
330/// This type preserves proof artifacts without establishing their validity.
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct ExecutionProof {
333    compatibility: ExecutionProofCompatibility,
334    vm: VmProof,
335    precompile: PrecompileStatus,
336}
337
338impl ExecutionProof {
339    /// Creates an execution proof with the compatibility declared by the current prover.
340    pub fn new(vm: VmProof, precompile: PrecompileStatus) -> Self {
341        Self::from_parts(ExecutionProofCompatibility::current(), vm, precompile)
342    }
343
344    /// Creates an execution proof from its compatibility, VM proof, and precompile state.
345    pub const fn from_parts(
346        compatibility: ExecutionProofCompatibility,
347        vm: VmProof,
348        precompile: PrecompileStatus,
349    ) -> Self {
350        Self { compatibility, vm, precompile }
351    }
352
353    /// Returns the compatibility declared by this proof.
354    pub const fn compatibility(&self) -> &ExecutionProofCompatibility {
355        &self.compatibility
356    }
357
358    /// Returns the VM proof.
359    pub const fn vm(&self) -> &VmProof {
360        &self.vm
361    }
362
363    /// Returns the state of the precompile work.
364    pub const fn precompile(&self) -> &PrecompileStatus {
365        &self.precompile
366    }
367
368    /// Splits this proof into its compatibility, VM proof, and precompile state.
369    pub fn into_parts(self) -> (ExecutionProofCompatibility, VmProof, PrecompileStatus) {
370        (self.compatibility, self.vm, self.precompile)
371    }
372
373    /// Returns whether this proof has completed its lifecycle transition.
374    pub const fn is_complete(&self) -> bool {
375        !matches!(self.precompile, PrecompileStatus::Deferred(_))
376    }
377
378    /// Returns whether this proof contains precompile work.
379    pub const fn has_precompiles(&self) -> bool {
380        !matches!(self.precompile, PrecompileStatus::Empty)
381    }
382
383    /// Transitions a deferred proof to complete by attaching a precompile proof.
384    ///
385    /// This method does not verify the precompile proof or check it against the VM proof. The
386    /// verifier performs those checks.
387    pub fn complete(mut self, precompile: PrecompileProof) -> Result<Self, ExecutionProofError> {
388        if !matches!(self.precompile, PrecompileStatus::Deferred(_)) {
389            return Err(ExecutionProofError::AlreadyComplete);
390        }
391        self.precompile = PrecompileStatus::Proven(precompile);
392        Ok(self)
393    }
394
395    /// Encodes this proof canonically.
396    pub fn to_bytes(&self) -> Vec<u8> {
397        Serializable::to_bytes(self)
398    }
399
400    /// Decodes a complete versioned proof from canonical bytes.
401    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
402        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
403        let proof = <Self as Deserializable>::read_from(&mut reader)?;
404
405        if reader.has_more_bytes() {
406            return Err(DeserializationError::InvalidValue(
407                "extra bytes after versioned proof payload".into(),
408            ));
409        }
410        if proof.to_bytes() != bytes {
411            return Err(DeserializationError::InvalidValue(
412                "versioned proof bytes are not canonically encoded".into(),
413            ));
414        }
415
416        Ok(proof)
417    }
418}
419
420impl Serializable for ExecutionProof {
421    fn write_into<W: ByteWriter>(&self, target: &mut W) {
422        target.write_u8(self.compatibility.format);
423        self.compatibility.vm_verifier_roots.write_into(target);
424        self.compatibility.pvm_verifier_roots.write_into(target);
425        self.write_into_v1(target);
426    }
427}
428
429impl Deserializable for ExecutionProof {
430    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
431        let format = source.read_u8()?;
432        if format != ExecutionProofCompatibility::FORMAT_V1 {
433            return Err(DeserializationError::InvalidValue(format!(
434                "unsupported execution proof format {format}"
435            )));
436        }
437
438        let vm_verifier_roots = Vec::<Word>::read_from(source)?;
439        let pvm_verifier_roots = Vec::<Word>::read_from(source)?;
440        let compatibility = ExecutionProofCompatibility::new(vm_verifier_roots, pvm_verifier_roots)
441            .map_err(|error| DeserializationError::InvalidValue(error.to_string()))?;
442
443        Self::read_from_v1(source, compatibility)
444    }
445
446    fn min_serialized_size() -> usize {
447        u8::min_serialized_size()
448            + Vec::<Word>::min_serialized_size()
449            + Vec::<Word>::min_serialized_size()
450            + ExecutionProof::min_serialized_size_v1()
451    }
452}
453
454impl ExecutionProof {
455    fn write_into_v1<W: ByteWriter>(&self, target: &mut W) {
456        match &self.precompile {
457            PrecompileStatus::Deferred(precompile) => {
458                target.write_u8(DEFERRED_PROOF_DISCRIMINANT);
459                self.vm.write_into(target);
460                precompile.write_into(target);
461            },
462            PrecompileStatus::Empty => {
463                target.write_u8(COMPLETE_PROOF_DISCRIMINANT);
464                self.vm.write_into(target);
465                Option::<PrecompileProof>::None.write_into(target);
466            },
467            PrecompileStatus::Proven(precompile) => {
468                target.write_u8(COMPLETE_PROOF_DISCRIMINANT);
469                self.vm.write_into(target);
470                Some(precompile).write_into(target);
471            },
472        }
473    }
474
475    fn read_from_v1<R: ByteReader>(
476        source: &mut R,
477        compatibility: ExecutionProofCompatibility,
478    ) -> Result<Self, DeserializationError> {
479        let discriminant = source.read_u8()?;
480        if !matches!(discriminant, DEFERRED_PROOF_DISCRIMINANT | COMPLETE_PROOF_DISCRIMINANT) {
481            return Err(DeserializationError::InvalidValue(format!(
482                "invalid execution proof discriminant {discriminant}"
483            )));
484        }
485
486        let vm = VmProof::read_from(source)?;
487        let precompile = match discriminant {
488            DEFERRED_PROOF_DISCRIMINANT => {
489                PrecompileStatus::Deferred(DeferredStateWire::read_from(source)?)
490            },
491            COMPLETE_PROOF_DISCRIMINANT => match Option::<PrecompileProof>::read_from(source)? {
492                Some(precompile) => PrecompileStatus::Proven(precompile),
493                None => PrecompileStatus::Empty,
494            },
495            _ => unreachable!("execution proof discriminant was checked before decoding"),
496        };
497
498        Ok(Self { compatibility, vm, precompile })
499    }
500
501    fn min_serialized_size_v1() -> usize {
502        u8::min_serialized_size()
503            + VmProof::min_serialized_size()
504            + Option::<PrecompileProof>::min_serialized_size()
505    }
506}
507
508/// Lifecycle errors returned while transitioning an execution proof.
509#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
510pub enum ExecutionProofError {
511    /// Only a deferred execution proof can be completed.
512    #[error("the execution proof is already complete")]
513    AlreadyComplete,
514}
515
516// STARK PROOF
517// ================================================================================================
518
519/// A serialized STARK proof and the hash function used during proof generation.
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct StarkProof {
522    bytes: Vec<u8>,
523    hash_fn: HashFunction,
524}
525
526impl StarkProof {
527    /// Creates a new instance of [StarkProof] from proof bytes and hash function.
528    pub const fn new(bytes: Vec<u8>, hash_fn: HashFunction) -> Self {
529        Self { bytes, hash_fn }
530    }
531
532    /// Returns the serialized STARK proof bytes.
533    pub fn bytes(&self) -> &[u8] {
534        &self.bytes
535    }
536
537    /// Returns the hash function used during proof generation process.
538    pub const fn hash_fn(&self) -> HashFunction {
539        self.hash_fn
540    }
541
542    /// Returns the serialized STARK proof bytes and hash function.
543    pub fn into_parts(self) -> (Vec<u8>, HashFunction) {
544        (self.bytes, self.hash_fn)
545    }
546}
547
548impl Serializable for StarkProof {
549    fn write_into<W: ByteWriter>(&self, target: &mut W) {
550        self.bytes.write_into(target);
551        self.hash_fn.write_into(target);
552    }
553}
554
555impl Deserializable for StarkProof {
556    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
557        let byte_count = source.read_usize()?;
558        if byte_count > MAX_STARK_PROOF_BYTES {
559            return Err(DeserializationError::InvalidValue(format!(
560                "STARK proof contains too many bytes: found {byte_count}, maximum is {MAX_STARK_PROOF_BYTES}"
561            )));
562        }
563        let bytes = source.read_many_iter::<u8>(byte_count)?.collect::<Result<Vec<_>, _>>()?;
564        let hash_fn = HashFunction::read_from(source)?;
565        Ok(Self::new(bytes, hash_fn))
566    }
567
568    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
569        let mut reader = BudgetedReader::new(SliceReader::new(bytes), bytes.len());
570        Self::read_from(&mut reader)
571    }
572
573    fn min_serialized_size() -> usize {
574        Vec::<u8>::min_serialized_size() + HashFunction::min_serialized_size()
575    }
576}
577
578// HELPER FUNCTIONS
579// ================================================================================================
580
581fn has_duplicate(roots: &[Word]) -> bool {
582    let mut unique = BTreeSet::new();
583    roots.iter().any(|root| !unique.insert(*root))
584}
585
586// TESTS
587// ================================================================================================
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::{
593        Felt,
594        deferred::{DeferredState, Node, PrecompileWitness, TRUE_DIGEST},
595        serde::ByteWriter,
596    };
597
598    fn dummy_stark_proof(bytes: &[u8]) -> StarkProof {
599        StarkProof::new(bytes.to_vec(), HashFunction::Blake3_256)
600    }
601
602    fn root(value: u64) -> DeferredRoot {
603        [Felt::new(value).unwrap(), Felt::ZERO, Felt::ZERO, Felt::ZERO].into()
604    }
605
606    fn vm_proof(precompile_root: DeferredRoot) -> VmProof {
607        VmProof {
608            proof: dummy_stark_proof(&[1]),
609            precompile_root,
610        }
611    }
612
613    fn precompile_proof(roots: &[DeferredRoot]) -> PrecompileProof {
614        PrecompileProof {
615            proof: dummy_stark_proof(&[2]),
616            roots: roots.to_vec(),
617        }
618    }
619
620    fn wire() -> (DeferredStateWire, DeferredRoot) {
621        let mut state = DeferredState::default();
622        let statement = state.register(Node::and(TRUE_DIGEST, TRUE_DIGEST)).unwrap();
623        state.log_statement(statement).unwrap();
624        let witness = PrecompileWitness::new(state).unwrap();
625        (witness.state().to_wire().unwrap(), witness.roots()[0])
626    }
627
628    fn versioned(vm: VmProof, precompile: PrecompileStatus) -> ExecutionProof {
629        ExecutionProof::from_parts(
630            ExecutionProofCompatibility::new(vec![root(11), root(12)], vec![root(21)]).unwrap(),
631            vm,
632            precompile,
633        )
634    }
635
636    fn version_prefix() -> Vec<u8> {
637        let compatibility =
638            ExecutionProofCompatibility::new(vec![root(11), root(12)], vec![root(21)]).unwrap();
639        let mut bytes = vec![compatibility.format()];
640        compatibility.vm_verifier_roots().to_vec().write_into(&mut bytes);
641        compatibility.pvm_verifier_roots().to_vec().write_into(&mut bytes);
642        bytes
643    }
644
645    #[test]
646    fn execution_proof_reports_precompile_state() {
647        let (precompile_wire, wire_root) = wire();
648        let deferred = versioned(vm_proof(wire_root), PrecompileStatus::Deferred(precompile_wire));
649        assert!(deferred.has_precompiles());
650
651        let complete_without_precompile = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
652        assert!(!complete_without_precompile.has_precompiles());
653
654        let complete_with_precompile =
655            versioned(vm_proof(root(1)), PrecompileStatus::Proven(precompile_proof(&[root(1)])));
656        assert!(complete_with_precompile.has_precompiles());
657    }
658
659    #[test]
660    fn versioned_proof_round_trips_with_format_first() {
661        let proof = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
662
663        let bytes = proof.to_bytes();
664        let decoded = ExecutionProof::read_from_bytes(&bytes).unwrap();
665
666        assert_eq!(bytes[0], ExecutionProofCompatibility::FORMAT_V1);
667        assert_eq!(decoded, proof);
668        assert_eq!(decoded.compatibility().format(), ExecutionProofCompatibility::FORMAT_V1);
669        assert_eq!(decoded.compatibility().vm_verifier_roots(), &[root(11), root(12)]);
670        assert_eq!(decoded.compatibility().pvm_verifier_roots(), &[root(21)]);
671    }
672
673    #[test]
674    fn versioned_proof_decoder_rejects_unknown_format_before_body() {
675        let error = ExecutionProof::read_from_bytes(&[ExecutionProofCompatibility::FORMAT_V1 + 1])
676            .unwrap_err();
677
678        assert!(
679            matches!(error, DeserializationError::InvalidValue(message) if message.contains("unsupported execution proof format 2"))
680        );
681    }
682
683    #[test]
684    fn versioned_proof_decoder_rejects_trailing_and_noncanonical_bytes() {
685        let proof = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
686        let mut trailing = proof.to_bytes();
687        trailing.push(0);
688        assert!(ExecutionProof::read_from_bytes(&trailing).is_err());
689
690        let canonical = proof.to_bytes();
691        assert_eq!(canonical[1], 5, "two VM roots use a one-byte vint encoding");
692        let mut noncanonical = alloc::vec![canonical[0], 0];
693        noncanonical.extend_from_slice(&2u64.to_le_bytes());
694        noncanonical.extend_from_slice(&canonical[2..]);
695        let error = ExecutionProof::read_from_bytes(&noncanonical).unwrap_err();
696        assert!(
697            matches!(error, DeserializationError::InvalidValue(message) if message.contains("not canonically encoded"))
698        );
699    }
700
701    #[test]
702    fn versioned_proof_decoder_applies_the_input_budget_to_root_lists() {
703        let mut bytes = vec![ExecutionProofCompatibility::FORMAT_V1];
704        bytes.write_usize(usize::MAX);
705
706        let error = ExecutionProof::read_from_bytes(&bytes).unwrap_err();
707        assert!(matches!(error, DeserializationError::InvalidValue(_)));
708    }
709
710    #[test]
711    fn compatibility_constructor_rejects_duplicate_roots() {
712        assert_eq!(
713            ExecutionProofCompatibility::new(vec![root(1), root(1)], vec![]),
714            Err(ExecutionProofCompatibilityError::DuplicateVmVerifierRoot)
715        );
716        assert_eq!(
717            ExecutionProofCompatibility::new(vec![], vec![root(2), root(2)]),
718            Err(ExecutionProofCompatibilityError::DuplicatePvmVerifierRoot)
719        );
720    }
721
722    #[test]
723    fn versioned_proof_decoder_rejects_duplicate_roots() {
724        let proof = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
725        let proof_bytes = proof.to_bytes();
726        let body = &proof_bytes[version_prefix().len()..];
727
728        let mut duplicate_vm = vec![ExecutionProofCompatibility::FORMAT_V1];
729        vec![root(1), root(1)].write_into(&mut duplicate_vm);
730        Vec::<Word>::new().write_into(&mut duplicate_vm);
731        duplicate_vm.extend_from_slice(body);
732        let error = ExecutionProof::read_from_bytes(&duplicate_vm).unwrap_err();
733        assert!(
734            matches!(error, DeserializationError::InvalidValue(message) if message.contains("VM verifier roots must not contain duplicates"))
735        );
736
737        let mut duplicate_pvm = vec![ExecutionProofCompatibility::FORMAT_V1];
738        Vec::<Word>::new().write_into(&mut duplicate_pvm);
739        vec![root(2), root(2)].write_into(&mut duplicate_pvm);
740        duplicate_pvm.extend_from_slice(body);
741        let error = ExecutionProof::read_from_bytes(&duplicate_pvm).unwrap_err();
742        assert!(
743            matches!(error, DeserializationError::InvalidValue(message) if message.contains("PVM verifier roots must not contain duplicates"))
744        );
745    }
746
747    #[test]
748    fn proof_minimum_serialized_sizes_match_shortest_canonical_encodings() {
749        let stark = StarkProof::new(Vec::new(), HashFunction::Blake3_256);
750        assert_eq!(StarkProof::min_serialized_size(), stark.to_bytes().len());
751        assert_eq!(StarkProof::min_serialized_size(), 2);
752
753        let vm = VmProof {
754            proof: stark,
755            precompile_root: TRUE_DIGEST,
756        };
757        assert_eq!(VmProof::min_serialized_size(), vm.to_bytes().len());
758        assert_eq!(VmProof::min_serialized_size(), 34);
759
760        let empty = PrecompileProof {
761            proof: StarkProof::new(Vec::new(), HashFunction::Blake3_256),
762            roots: Vec::new(),
763        };
764        assert_eq!(PrecompileProof::min_serialized_size(), empty.to_bytes().len());
765        assert_eq!(PrecompileProof::min_serialized_size(), 3);
766
767        let singleton = PrecompileProof {
768            proof: StarkProof::new(Vec::new(), HashFunction::Blake3_256),
769            roots: alloc::vec![root(1)],
770        };
771        assert_eq!(singleton.to_bytes().len(), 35);
772
773        let proofs = alloc::vec![singleton.clone(), singleton];
774        let bytes = proofs.to_bytes();
775        assert_eq!(bytes.len(), 71);
776        let decoded = Vec::<PrecompileProof>::read_from_bytes_with_budget(&bytes, 71).unwrap();
777        assert_eq!(decoded.to_bytes(), bytes);
778    }
779
780    #[test]
781    fn stark_proof_decoder_rejects_oversized_length_before_payload() {
782        let mut bytes = Vec::new();
783        bytes.write_usize(MAX_STARK_PROOF_BYTES + 1);
784
785        let error = StarkProof::read_from_bytes(&bytes).unwrap_err();
786        let DeserializationError::InvalidValue(message) = error else {
787            panic!("expected excessive STARK proof length to be rejected")
788        };
789        assert!(message.contains("STARK proof contains too many bytes"));
790    }
791
792    #[test]
793    fn precompile_proof_decoder_rejects_oversized_root_count_before_payload() {
794        let mut bytes = dummy_stark_proof(&[2]).to_bytes();
795        bytes.write_usize(MAX_PRECOMPILE_ROOTS + 1);
796
797        let error = PrecompileProof::read_from_bytes(&bytes).unwrap_err();
798        let DeserializationError::InvalidValue(message) = error else {
799            panic!("expected excessive root count to be rejected")
800        };
801        assert!(message.contains("precompile proof contains too many roots"));
802    }
803
804    #[test]
805    fn standalone_proof_decoders_reject_trailing_bytes() {
806        let mut vm_bytes = vm_proof(root(3)).to_bytes();
807        vm_bytes.push(0);
808        assert!(VmProof::read_from_bytes(&vm_bytes).is_err());
809
810        let mut precompile_bytes = precompile_proof(&[root(3)]).to_bytes();
811        precompile_bytes.push(0);
812        assert!(PrecompileProof::read_from_bytes(&precompile_bytes).is_err());
813    }
814
815    #[test]
816    fn proof_artifacts_round_trip_canonically() {
817        let stark = dummy_stark_proof(&[1, 2, 3]);
818        let stark_bytes = stark.to_bytes();
819        let decoded_stark = StarkProof::read_from_bytes(&stark_bytes).unwrap();
820        assert_eq!(decoded_stark.to_bytes(), stark_bytes);
821
822        let vm = vm_proof(root(3));
823        let vm_bytes = vm.to_bytes();
824        let decoded_vm = VmProof::read_from_bytes(&vm_bytes).unwrap();
825        assert_eq!(decoded_vm.to_bytes(), vm_bytes);
826
827        let precompile = precompile_proof(&[]);
828        let precompile_bytes = precompile.to_bytes();
829        let decoded_precompile = PrecompileProof::read_from_bytes(&precompile_bytes).unwrap();
830        assert_eq!(decoded_precompile.to_bytes(), precompile_bytes);
831
832        let (precompile_wire, wire_root) = wire();
833        let proofs = [
834            versioned(vm_proof(wire_root), PrecompileStatus::Deferred(precompile_wire)),
835            versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty),
836            versioned(
837                vm_proof(TRUE_DIGEST),
838                PrecompileStatus::Proven(precompile_proof(&[TRUE_DIGEST])),
839            ),
840        ];
841        for proof in proofs {
842            let bytes = proof.to_bytes();
843            assert_eq!(ExecutionProof::read_from_bytes(&bytes).unwrap(), proof);
844        }
845    }
846
847    #[test]
848    fn complete_transitions_deferred_proof_without_validating_artifact_shape() {
849        let vm = vm_proof(TRUE_DIGEST);
850        let precompile = precompile_proof(&[]);
851        let deferred =
852            versioned(vm.clone(), PrecompileStatus::Deferred(DeferredStateWire::default()));
853
854        let completed = deferred.complete(precompile.clone()).unwrap();
855
856        let (completed_compatibility, completed_vm, completed_precompile) = completed.into_parts();
857        let PrecompileStatus::Proven(completed_precompile) = completed_precompile else {
858            panic!("deferred proof should transition to complete")
859        };
860        assert_eq!(
861            completed_compatibility,
862            ExecutionProofCompatibility::new(vec![root(11), root(12)], vec![root(21)]).unwrap()
863        );
864        assert_eq!(completed_vm.to_bytes(), vm.to_bytes());
865        assert_eq!(completed_precompile.to_bytes(), precompile.to_bytes());
866    }
867
868    #[test]
869    fn complete_rejects_an_already_complete_proof() {
870        let complete = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty);
871
872        assert!(matches!(
873            complete.complete(precompile_proof(&[])),
874            Err(ExecutionProofError::AlreadyComplete)
875        ));
876    }
877
878    #[test]
879    fn versioned_proof_transport_rejects_bad_discriminants_and_stark_bounds() {
880        let mut bad_discriminant = version_prefix();
881        bad_discriminant.write_u8(9);
882        assert!(ExecutionProof::read_from_bytes(&bad_discriminant).is_err());
883
884        let canonical = versioned(vm_proof(TRUE_DIGEST), PrecompileStatus::Empty).to_bytes();
885        let prefix_len = version_prefix().len();
886        assert_eq!(canonical[prefix_len + 1], 3, "one STARK byte uses a one-byte vint encoding");
887        let mut noncanonical = canonical[..prefix_len + 1].to_vec();
888        noncanonical.push(0);
889        noncanonical.extend_from_slice(&1u64.to_le_bytes());
890        noncanonical.extend_from_slice(&canonical[prefix_len + 2..]);
891        let error = ExecutionProof::read_from_bytes(&noncanonical).unwrap_err();
892        assert!(
893            matches!(error, DeserializationError::InvalidValue(message) if message.contains("not canonically encoded"))
894        );
895
896        let mut oversized_proof = version_prefix();
897        oversized_proof.write_u8(COMPLETE_PROOF_DISCRIMINANT);
898        oversized_proof.write_usize(MAX_STARK_PROOF_BYTES + 1);
899        let error = ExecutionProof::read_from_bytes(&oversized_proof).unwrap_err();
900        assert!(
901            matches!(error, DeserializationError::InvalidValue(message) if message.contains("STARK proof contains too many bytes"))
902        );
903    }
904}