Skip to main content

miden_verifier/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5#[cfg(feature = "std")]
6extern crate std;
7
8use alloc::{boxed::Box, sync::Arc};
9
10use miden_air::{MidenMultiAir, PublicInputs, Statement, config, security};
11use miden_core::{
12    Felt,
13    deferred::{
14        DeferredRoot, MAX_PRECOMPILE_ROOTS, PrecompileError, PrecompileRegistry, TRUE_DIGEST,
15    },
16    field::QuadFelt,
17    proof::{CURRENT_PVM_VERIFIER_ROOT, CURRENT_VM_VERIFIER_ROOT, MAX_STARK_PROOF_BYTES},
18};
19use miden_crypto::stark::{
20    StarkConfig, VerifierInstance, lmcs::Lmcs, proof::StarkProofData, verifier::VerifierError,
21};
22use miden_serde_utils::deserialize_schema_exact;
23use serde::de::DeserializeOwned;
24use serde_wincode::{SerdeCompat, wincode};
25
26// RE-EXPORTS
27// ================================================================================================
28mod exports {
29    pub use miden_core::{
30        Word,
31        program::{ExecutionClaim, KernelDescriptor, ProgramInfo, StackInputs, StackOutputs},
32        proof::{
33            ExecutionProof, ExecutionProofCompatibility, ExecutionProofCompatibilityError,
34            HashFunction, PrecompileProof, PrecompileStatus, StarkProof, VmProof,
35        },
36    };
37    pub mod math {
38        pub use miden_core::Felt;
39    }
40}
41pub use exports::*;
42pub use miden_air::security::{
43    AirShape, InstanceShape, LookupShape, ProofSecurityParameters, ProtocolParams, SecurityReport,
44    SecurityTerm,
45};
46
47pub mod recursive;
48
49struct VerifierSupport {
50    format: u8,
51    accepted_vm_roots: &'static [Word],
52    accepted_pvm_roots: &'static [Word],
53}
54
55impl VerifierSupport {
56    fn check(&self, proof: &ExecutionProof) -> Result<(), VerificationError> {
57        let compatibility = proof.compatibility();
58        if compatibility.format() != self.format {
59            return Err(VerificationError::UnsupportedProofFormat(compatibility.format()));
60        }
61        if !roots_overlap(compatibility.vm_verifier_roots(), self.accepted_vm_roots) {
62            return Err(VerificationError::IncompatibleVmVerifier);
63        }
64        if !roots_overlap(compatibility.pvm_verifier_roots(), self.accepted_pvm_roots) {
65            return Err(VerificationError::IncompatiblePvmVerifier);
66        }
67
68        Ok(())
69    }
70}
71
72const VERIFIER_SUPPORT_V2: VerifierSupport = VerifierSupport {
73    format: ExecutionProofCompatibility::FORMAT_V2,
74    accepted_vm_roots: &[CURRENT_VM_VERIFIER_ROOT],
75    accepted_pvm_roots: &[CURRENT_PVM_VERIFIER_ROOT],
76};
77
78// VERIFIER
79// ================================================================================================
80
81/// Verifier for deferred and complete Miden execution proofs.
82#[derive(Debug, Clone)]
83pub struct Verifier {
84    precompile_registry: Arc<PrecompileRegistry>,
85    min_security_level: Option<u32>,
86}
87
88impl Verifier {
89    /// Creates a verifier with the canonical precompile registry and verification limits, and no
90    /// minimum security level.
91    pub fn new() -> Self {
92        Self {
93            precompile_registry: Arc::new(miden_precompiles::registry()),
94            min_security_level: None,
95        }
96    }
97
98    /// Requires at least `min_security_level` bits of conjectured security for each verified STARK.
99    ///
100    /// The minimum applies independently to the VM STARK and any supplied precompile STARK,
101    /// including standalone calls to [`Self::verify_precompile`]. It is checked using authenticated
102    /// parameters after each STARK verifies. The VM check precedes deferred witness evaluation.
103    /// Successful results still return the actual authenticated security parameters.
104    pub fn with_min_conjectured_security_level_per_stark(
105        mut self,
106        min_security_level: u32,
107    ) -> Self {
108        self.min_security_level = Some(min_security_level);
109        self
110    }
111
112    /// Returns the compatibility declared by proofs produced by the current prover.
113    pub fn proof_compatibility() -> ExecutionProofCompatibility {
114        ExecutionProofCompatibility::current()
115    }
116
117    /// Verifies a deferred or complete versioned execution proof against its public claim.
118    ///
119    /// The VM STARK authenticates the carried precompile root in either state. For a deferred
120    /// proof, the verifier first verifies the VM STARK, then evaluates the carried
121    /// `PrecompileWitness` and checks that its recomputed root matches the VM root. It returns the
122    /// authenticated root as an outstanding obligation until a precompile STARK is supplied.
123    /// Complete proofs that contain precompile work additionally verify the aggregate precompile
124    /// STARK against the VM-authenticated root.
125    ///
126    /// The outcome reports the authenticated security parameters of the components actually
127    /// verified and any precompile root that remains outstanding. Callers can use
128    /// [`ProofSecurityParameters::conjectured_security_level`] to estimate each verified proof's
129    /// conjectured security level. If configured, the minimum security level is enforced for each
130    /// verified STARK, before evaluating a deferred witness in the VM's case.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the proof structure is invalid, a required STARK rejects, deferred
135    /// witness evaluation fails, the witness root does not match the VM-authenticated root, or a
136    /// verified STARK's conjectured security level is below the configured minimum.
137    pub fn verify(
138        &self,
139        claim: &ExecutionClaim,
140        proof: &ExecutionProof,
141    ) -> Result<VerificationOutcome, VerificationError> {
142        match proof.compatibility().format() {
143            ExecutionProofCompatibility::FORMAT_V2 => {
144                VERIFIER_SUPPORT_V2.check(proof)?;
145                self.verify_v2(claim, proof)
146            },
147            format => Err(VerificationError::UnsupportedProofFormat(format)),
148        }
149    }
150
151    /// Verifies an execution proof encoded with transport format 2.
152    fn verify_v2(
153        &self,
154        claim: &ExecutionClaim,
155        proof: &ExecutionProof,
156    ) -> Result<VerificationOutcome, VerificationError> {
157        let vm = proof.vm();
158        let (outstanding_root, precompile) = match proof.precompile() {
159            PrecompileStatus::Deferred(_) => {
160                let root = vm.precompile_root;
161                if root == TRUE_DIGEST {
162                    return Err(VerificationError::DeferredTrueRoot);
163                }
164                (Some(root), None)
165            },
166            PrecompileStatus::Empty => {
167                let vm_root = vm.precompile_root;
168                if vm_root != TRUE_DIGEST {
169                    return Err(VerificationError::MissingPrecompileProof);
170                }
171                (None, None)
172            },
173            PrecompileStatus::Proven(precompile) => {
174                self.validate_precompile(precompile, vm.precompile_root)?;
175                (None, Some(precompile))
176            },
177        };
178
179        self.preflight_vm_stark(claim, vm)?;
180        if let Some(precompile) = precompile {
181            self.preflight_precompile_stark(precompile)?;
182        }
183
184        let vm_security_parameters = self.verify_vm(claim, vm)?;
185        self.check_security_level(&vm_security_parameters)?;
186        // Authenticate the VM statement and enforce the configured minimum security before
187        // performing potentially expensive witness evaluation.
188        if let PrecompileStatus::Deferred(witness) = proof.precompile()
189            && (witness.root_unchecked() != vm.precompile_root
190                || witness.compute_root(Arc::clone(&self.precompile_registry))?
191                    != vm.precompile_root)
192        {
193            return Err(VerificationError::DeferredWitnessRootMismatch);
194        }
195        let precompile_security_parameters = precompile
196            .map(|precompile| self.verify_precompile(precompile, vm.precompile_root))
197            .transpose()?;
198
199        Ok(VerificationOutcome::new(
200            vm_security_parameters,
201            precompile_security_parameters,
202            outstanding_root,
203        ))
204    }
205
206    /// Verifies a precompile proof against an expected outstanding execution root.
207    ///
208    /// The expected root may occur anywhere in the proof's ordered constituent roots. All roots,
209    /// including compatible extras and duplicate occurrences, are folded from the first root to
210    /// derive the aggregate precompile STARK statement. On success, this returns the precompile
211    /// STARK's authenticated security parameters, after enforcing the configured minimum security
212    /// level, if any.
213    ///
214    /// The expected root and every constituent root must differ from [`TRUE_DIGEST`].
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the artifact shape or expected-root coverage is invalid, or if the
219    /// precompile STARK rejects or its conjectured security level is below the configured minimum.
220    pub fn verify_precompile(
221        &self,
222        proof: &PrecompileProof,
223        expected_root: DeferredRoot,
224    ) -> Result<ProofSecurityParameters, VerificationError> {
225        self.validate_precompile(proof, expected_root)?;
226        self.preflight_precompile_stark(proof)?;
227
228        let aggregate_root =
229            proof.aggregate_root().expect("precompile roots were checked to be non-empty");
230        let security_parameters =
231            miden_precompiles_verifier::verify_deferred(&proof.proof, aggregate_root)?;
232        self.check_security_level(&security_parameters)?;
233        Ok(security_parameters)
234    }
235
236    fn check_security_level(
237        &self,
238        parameters: &ProofSecurityParameters,
239    ) -> Result<(), VerificationError> {
240        if let Some(required) = self.min_security_level {
241            let actual = parameters.conjectured_security_level();
242            if actual < required {
243                return Err(VerificationError::InsufficientSecurityLevel { actual, required });
244            }
245        }
246        Ok(())
247    }
248
249    fn validate_precompile(
250        &self,
251        proof: &PrecompileProof,
252        expected_root: DeferredRoot,
253    ) -> Result<(), VerificationError> {
254        let roots = &proof.roots;
255        if roots.is_empty() {
256            return Err(VerificationError::EmptyPrecompileRoots);
257        }
258        if roots.len() > MAX_PRECOMPILE_ROOTS {
259            return Err(VerificationError::TooManyPrecompileRoots {
260                roots: roots.len(),
261                max: MAX_PRECOMPILE_ROOTS,
262            });
263        }
264        if let Some(index) = roots.iter().position(|root| *root == TRUE_DIGEST) {
265            return Err(VerificationError::SettledPrecompileRoot { index });
266        }
267        if expected_root == TRUE_DIGEST {
268            return Err(VerificationError::UnexpectedPrecompileProof);
269        }
270        if !roots.contains(&expected_root) {
271            return Err(VerificationError::InsufficientPrecompileRootCoverage);
272        }
273
274        Ok(())
275    }
276
277    fn preflight_vm_stark(
278        &self,
279        claim: &ExecutionClaim,
280        proof: &VmProof,
281    ) -> Result<(), VerificationError> {
282        let size = proof.proof.bytes().len();
283        if size > MAX_STARK_PROOF_BYTES {
284            return Err(VerificationError::StarkVerificationError(
285                claim.program_root(),
286                Box::new(StarkVerificationError::ProofTooLarge {
287                    size,
288                    max: MAX_STARK_PROOF_BYTES,
289                }),
290            ));
291        }
292        Ok(())
293    }
294
295    fn preflight_precompile_stark(&self, proof: &PrecompileProof) -> Result<(), VerificationError> {
296        let size = proof.proof.bytes().len();
297        if size > MAX_STARK_PROOF_BYTES {
298            return Err(VerificationError::PrecompileStarkVerification(
299                miden_precompiles_verifier::VerifyError::ProofTooLarge {
300                    size,
301                    max: MAX_STARK_PROOF_BYTES,
302                },
303            ));
304        }
305        Ok(())
306    }
307
308    /// Verifies the Miden VM STARK proof and returns its authenticated security parameters.
309    ///
310    /// The returned parameters include the largest AIR trace height, the DEEP term count implied
311    /// by the commitment scheme's column alignment, and the lookup boundary terms implied by the
312    /// authenticated kernel. The PCS parameters and commitment collision resistance come from the
313    /// configuration used to verify the proof.
314    fn verify_vm(
315        &self,
316        claim: &ExecutionClaim,
317        proof: &VmProof,
318    ) -> Result<ProofSecurityParameters, VerificationError> {
319        let program_root = claim.program_root();
320        let pub_inputs = PublicInputs::new(
321            claim.to_program_info(),
322            *claim.stack_inputs(),
323            *claim.stack_outputs(),
324            proof.precompile_root,
325        );
326        let (public_values, aux_inputs) = pub_inputs.to_air_inputs();
327
328        let stark = &proof.proof;
329        let proof_bytes = stark.bytes();
330        let pcs_params = config::pcs_params();
331        let num_kernel_procedures = claim.kernel().proc_hashes().len() as u32;
332        match stark.hash_fn() {
333            HashFunction::Blake3_256 => {
334                let config = config::blake3_256_config(pcs_params, config::RELATION_DIGEST);
335                self.verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
336            },
337            HashFunction::Rpo256 => {
338                let config = config::rpo_config(pcs_params, config::RELATION_DIGEST);
339                self.verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
340            },
341            HashFunction::Rpx256 => {
342                let config = config::rpx_config(pcs_params, config::RELATION_DIGEST);
343                self.verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
344            },
345            HashFunction::Poseidon2 => {
346                let config = config::poseidon2_config(pcs_params, config::RELATION_DIGEST);
347                self.verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
348            },
349            HashFunction::Keccak => {
350                let config = config::keccak_config(pcs_params, config::RELATION_DIGEST);
351                self.verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
352            },
353        }
354        .map_err(|error| VerificationError::StarkVerificationError(program_root, Box::new(error)))
355        .map(|(log_max_height, alignment)| {
356            security::proof_security_parameters(
357                &pcs_params,
358                log_max_height,
359                num_kernel_procedures,
360                alignment,
361                stark.hash_fn().collision_resistance(),
362            )
363        })
364    }
365
366    /// Verifies a multi-AIR STARK proof for the Miden VM statement, returning the proof's largest
367    /// AIR log height and the LMCS's column alignment for grading.
368    ///
369    /// Pre-seeds the challenger with protocol parameters, AIR public values, and statement
370    /// `aux_inputs` (program hash, final deferred root, and kernel-procedure digests). Then
371    /// delegates to the lifted multi-AIR verifier.
372    fn verify_stark_proof<SC>(
373        &self,
374        config: &SC,
375        public_values: &[Felt],
376        aux_inputs: &[Felt],
377        proof_bytes: &[u8],
378    ) -> Result<(u32, usize), StarkVerificationError>
379    where
380        SC: StarkConfig<Felt, QuadFelt>,
381        <SC::Lmcs as Lmcs>::Commitment: DeserializeOwned,
382    {
383        if proof_bytes.len() > MAX_STARK_PROOF_BYTES {
384            return Err(StarkVerificationError::ProofTooLarge {
385                size: proof_bytes.len(),
386                max: MAX_STARK_PROOF_BYTES,
387            });
388        }
389
390        let proof_encoding_config = wincode::config::Configuration::default()
391            .with_preallocation_size_limit::<MAX_STARK_PROOF_BYTES>();
392        let proof = deserialize_schema_exact::<SerdeCompat<StarkProofData<Felt, QuadFelt, SC>>, _>(
393            proof_bytes,
394            proof_encoding_config,
395        )?;
396
397        let mut challenger = config.challenger();
398        config::observe_protocol_params(config.pcs(), &mut challenger);
399
400        // `air_inputs` are the public values read by the AIRs (stack i/o); `aux_inputs` are the
401        // statement inputs read during observation/boundary correction. The lifted verifier absorbs
402        // both into Fiat-Shamir internally, and derives the multi-AIR ordering deterministically
403        // from the proof's per-AIR trace heights.
404        let statement = Statement::<Felt, QuadFelt, _>::new(
405            MidenMultiAir::new(),
406            public_values.to_vec(),
407            aux_inputs.to_vec(),
408        )
409        .map_err(|error| StarkVerificationError::Verifier(VerifierError::from(error)))?;
410
411        VerifierInstance::new(config, &statement, None)
412            .expect("Miden AIRs declare no preprocessed columns")
413            .verify(&proof, challenger)?;
414
415        let log_max_height =
416            u32::from(proof.log_trace_heights().iter().copied().max().unwrap_or(0));
417        Ok((log_max_height, config.lmcs().alignment()))
418    }
419}
420
421impl Default for Verifier {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427/// Result of fully verifying an execution proof and all supplied STARKs.
428#[must_use = "verification may leave an outstanding precompile obligation"]
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub struct VerificationOutcome {
431    vm_security_parameters: ProofSecurityParameters,
432    precompile_security_parameters: Option<ProofSecurityParameters>,
433    outstanding_precompile_root: Option<DeferredRoot>,
434}
435
436impl VerificationOutcome {
437    const fn new(
438        vm_security_parameters: ProofSecurityParameters,
439        precompile_security_parameters: Option<ProofSecurityParameters>,
440        outstanding_precompile_root: Option<DeferredRoot>,
441    ) -> Self {
442        Self {
443            vm_security_parameters,
444            precompile_security_parameters,
445            outstanding_precompile_root,
446        }
447    }
448
449    /// Returns the authenticated security parameters of the verified MVM proof.
450    pub const fn vm_security_parameters(&self) -> &ProofSecurityParameters {
451        &self.vm_security_parameters
452    }
453
454    /// Returns the authenticated security parameters if verification included a PVM proof.
455    pub const fn precompile_security_parameters(&self) -> Option<&ProofSecurityParameters> {
456        self.precompile_security_parameters.as_ref()
457    }
458
459    /// Returns whether this verified outcome has no outstanding precompile obligation.
460    ///
461    /// This result is produced only after verifier-owned shape validation and verification of every
462    /// required STARK.
463    pub const fn is_complete(&self) -> bool {
464        self.outstanding_precompile_root.is_none()
465    }
466
467    /// Returns the authenticated precompile root that remains to be proved, if any.
468    pub const fn outstanding_precompile_root(&self) -> Option<DeferredRoot> {
469        self.outstanding_precompile_root
470    }
471}
472
473// ERRORS
474// ================================================================================================
475
476/// Errors that can occur during proof verification.
477#[derive(Debug, thiserror::Error)]
478#[non_exhaustive]
479pub enum VerificationError {
480    #[error("conjectured security level is {actual} bits, below the required {required} bits")]
481    InsufficientSecurityLevel { actual: u32, required: u32 },
482    #[error("execution proof format {0} is not supported")]
483    UnsupportedProofFormat(u8),
484    #[error("execution proof does not name a compatible VM verifier")]
485    IncompatibleVmVerifier,
486    #[error("execution proof does not name a compatible PVM verifier")]
487    IncompatiblePvmVerifier,
488    #[error("failed to verify VM STARK proof for program with hash {0}")]
489    StarkVerificationError(Word, #[source] Box<StarkVerificationError>),
490    #[error("a deferred execution proof cannot authenticate TRUE_DIGEST")]
491    DeferredTrueRoot,
492    #[error("deferred witness root does not match the VM obligation")]
493    DeferredWitnessRootMismatch,
494    #[error("deferred witness evaluation failed: {0}")]
495    DeferredWitnessEvaluation(#[from] PrecompileError),
496    #[error("a precompile proof must contain at least one constituent root")]
497    EmptyPrecompileRoots,
498    #[error("precompile proof contains too many roots: found {roots}, maximum is {max}")]
499    TooManyPrecompileRoots { roots: usize, max: usize },
500    #[error("precompile proof constituent root at index {index} is already settled")]
501    SettledPrecompileRoot { index: usize },
502    #[error("a precompile proof was supplied for an already settled VM obligation")]
503    UnexpectedPrecompileProof,
504    #[error("a precompile proof is required for a non-empty VM obligation")]
505    MissingPrecompileProof,
506    #[error("precompile proof roots do not cover the VM obligation")]
507    InsufficientPrecompileRootCoverage,
508    #[error("failed to verify aggregate precompile STARK proof: {0}")]
509    PrecompileStarkVerification(#[from] miden_precompiles_verifier::VerifyError),
510}
511
512/// Errors that can occur during low-level STARK proof verification.
513#[derive(Debug, thiserror::Error)]
514pub enum StarkVerificationError {
515    #[error("failed to deserialize proof: {0}")]
516    Deserialization(#[from] wincode::error::ReadError),
517    #[error("STARK proof is too large: {size} bytes exceeds the {max} byte limit")]
518    ProofTooLarge { size: usize, max: usize },
519    #[error(transparent)]
520    Verifier(#[from] VerifierError),
521}
522
523// HELPER FUNCTIONS
524// ================================================================================================
525
526fn roots_overlap(proof_roots: &[Word], accepted_roots: &[Word]) -> bool {
527    proof_roots.iter().any(|root| accepted_roots.contains(root))
528}
529
530// TESTS
531// ================================================================================================
532
533#[cfg(test)]
534mod tests {
535    use alloc::{vec, vec::Vec};
536
537    use miden_core::deferred::{PrecompileWitness, PrecompileWitnessEntry, Tag};
538
539    use super::*;
540
541    fn claim() -> ExecutionClaim {
542        ExecutionClaim::from_program_info(
543            ProgramInfo::default(),
544            StackInputs::default(),
545            StackOutputs::default(),
546        )
547    }
548
549    fn root(value: u64) -> Word {
550        [
551            Felt::new(value).unwrap(),
552            Felt::new(0).unwrap(),
553            Felt::new(0).unwrap(),
554            Felt::new(0).unwrap(),
555        ]
556        .into()
557    }
558
559    fn vm_proof(precompile_root: Word) -> VmProof {
560        VmProof {
561            proof: StarkProof::new(vec![0, 0], HashFunction::Blake3_256),
562            precompile_root,
563        }
564    }
565
566    fn precompile_proof(roots: Vec<Word>) -> PrecompileProof {
567        PrecompileProof {
568            proof: StarkProof::new(vec![0, 0], HashFunction::Poseidon2),
569            roots,
570        }
571    }
572
573    fn complete(vm_root: Word, roots: Option<Vec<Word>>) -> ExecutionProof {
574        let precompile = match roots {
575            Some(roots) => PrecompileStatus::Proven(precompile_proof(roots)),
576            None => PrecompileStatus::Empty,
577        };
578        ExecutionProof::new(vm_proof(vm_root), precompile)
579    }
580
581    #[test]
582    fn verifier_owns_shape_policy() {
583        type CheckError = fn(VerificationError) -> bool;
584
585        let required = root(1);
586        let deferred = PrecompileStatus::Deferred(
587            PrecompileWitness::from_entries(vec![PrecompileWitnessEntry::Join {
588                tag: Tag::AND,
589                lhs: 0,
590                rhs: 0,
591            }])
592            .expect("a logged TRUE is a nonempty obligation"),
593        );
594        let cases: Vec<(ExecutionProof, CheckError)> = vec![
595            (ExecutionProof::new(vm_proof(TRUE_DIGEST), deferred), |error| {
596                matches!(error, VerificationError::DeferredTrueRoot)
597            }),
598            (complete(required, Some(vec![])), |error| {
599                matches!(error, VerificationError::EmptyPrecompileRoots)
600            }),
601            (complete(required, Some(vec![required; MAX_PRECOMPILE_ROOTS + 1])), |error| {
602                matches!(
603                    error,
604                    VerificationError::TooManyPrecompileRoots { roots, max }
605                        if roots == MAX_PRECOMPILE_ROOTS + 1 && max == MAX_PRECOMPILE_ROOTS
606                )
607            }),
608            (complete(required, Some(vec![root(2), TRUE_DIGEST, required])), |error| {
609                matches!(error, VerificationError::SettledPrecompileRoot { index: 1 })
610            }),
611            (complete(required, None), |error| {
612                matches!(error, VerificationError::MissingPrecompileProof)
613            }),
614            (complete(TRUE_DIGEST, Some(vec![required])), |error| {
615                matches!(error, VerificationError::UnexpectedPrecompileProof)
616            }),
617            (complete(root(99), Some(vec![required])), |error| {
618                matches!(error, VerificationError::InsufficientPrecompileRootCoverage)
619            }),
620        ];
621
622        for (proof, check) in cases {
623            let error = Verifier::new().verify(&claim(), &proof).unwrap_err();
624            assert!(check(error));
625        }
626    }
627
628    #[test]
629    fn invalid_vm_stark_is_rejected_before_deferred_witness_evaluation() {
630        use miden_precompiles::{UintDomain, UintPrecompile};
631
632        let witness = PrecompileWitness::from_entries(vec![
633            PrecompileWitnessEntry::Data {
634                tag: UintPrecompile::value_tag(UintDomain::U256),
635                chunks: vec![[Felt::from_u32(0); 8]],
636            },
637            PrecompileWitnessEntry::Data {
638                tag: UintPrecompile::value_tag(UintDomain::U256),
639                chunks: vec![core::array::from_fn(|i| Felt::from_u32(u32::from(i == 0)))],
640            },
641            PrecompileWitnessEntry::Join {
642                tag: UintPrecompile::op_tag(UintPrecompile::EQ_OP_ID),
643                lhs: 1,
644                rhs: 2,
645            },
646            PrecompileWitnessEntry::Join { tag: Tag::AND, lhs: 0, rhs: 3 },
647        ])
648        .unwrap();
649        // Evaluating this witness first would reject its false assertion instead of the VM STARK.
650        assert!(matches!(
651            witness.compute_root(Arc::new(miden_precompiles::registry())),
652            Err(error) if matches!(error.root(), PrecompileError::AssertionFailed)
653        ));
654        let proof = ExecutionProof::new(
655            vm_proof(witness.root_unchecked()),
656            PrecompileStatus::Deferred(witness),
657        );
658        assert!(matches!(
659            Verifier::new().verify(&claim(), &proof),
660            Err(VerificationError::StarkVerificationError(..))
661        ));
662    }
663
664    #[test]
665    fn precompile_verifier_owns_artifact_shape_policy() {
666        type CheckError = fn(VerificationError) -> bool;
667
668        let required = root(1);
669        let cases: Vec<(PrecompileProof, Word, CheckError)> = vec![
670            (precompile_proof(vec![]), required, |error| {
671                matches!(error, VerificationError::EmptyPrecompileRoots)
672            }),
673            (precompile_proof(vec![required; MAX_PRECOMPILE_ROOTS + 1]), required, |error| {
674                matches!(
675                    error,
676                    VerificationError::TooManyPrecompileRoots { roots, max }
677                        if roots == MAX_PRECOMPILE_ROOTS + 1 && max == MAX_PRECOMPILE_ROOTS
678                )
679            }),
680            (precompile_proof(vec![required, TRUE_DIGEST]), required, |error| {
681                matches!(error, VerificationError::SettledPrecompileRoot { index: 1 })
682            }),
683            (precompile_proof(vec![required]), TRUE_DIGEST, |error| {
684                matches!(error, VerificationError::UnexpectedPrecompileProof)
685            }),
686            (precompile_proof(vec![required]), root(99), |error| {
687                matches!(error, VerificationError::InsufficientPrecompileRootCoverage)
688            }),
689        ];
690
691        for (proof, expected_root, check) in cases {
692            let error = Verifier::new().verify_precompile(&proof, expected_root).unwrap_err();
693            assert!(check(error));
694        }
695    }
696
697    #[test]
698    fn oversized_precompile_stark_is_rejected_before_vm_stark_verification() {
699        let required = root(1);
700        let proof = ExecutionProof::new(
701            vm_proof(required),
702            PrecompileStatus::Proven(PrecompileProof {
703                proof: StarkProof::new(vec![0; MAX_STARK_PROOF_BYTES + 1], HashFunction::Poseidon2),
704                roots: vec![required],
705            }),
706        );
707
708        let error = Verifier::new().verify(&claim(), &proof).unwrap_err();
709        assert!(matches!(
710            error,
711            VerificationError::PrecompileStarkVerification(
712                miden_precompiles_verifier::VerifyError::ProofTooLarge { size, max }
713            ) if size == MAX_STARK_PROOF_BYTES + 1 && max == MAX_STARK_PROOF_BYTES
714        ));
715    }
716
717    #[test]
718    fn malformed_transport_round_trips_then_verifier_rejects_it() {
719        let malformed = complete(root(1), Some(vec![]));
720        let bytes = malformed.to_bytes();
721        let decoded = ExecutionProof::read_from_bytes(&bytes).unwrap();
722
723        assert_eq!(decoded.to_bytes(), bytes);
724        assert!(matches!(
725            Verifier::new().verify(&claim(), &decoded),
726            Err(VerificationError::EmptyPrecompileRoots)
727        ));
728    }
729
730    #[test]
731    fn verifier_rejects_oversized_directly_constructed_vm_proof() {
732        let proof = ExecutionProof::new(
733            VmProof {
734                proof: StarkProof::new(
735                    vec![0; MAX_STARK_PROOF_BYTES + 1],
736                    HashFunction::Blake3_256,
737                ),
738                precompile_root: TRUE_DIGEST,
739            },
740            PrecompileStatus::Empty,
741        );
742
743        let error = Verifier::new().verify(&claim(), &proof).unwrap_err();
744        let VerificationError::StarkVerificationError(_, source) = error else {
745            panic!("expected oversized VM STARK proof to be rejected")
746        };
747        assert!(matches!(
748            *source,
749            StarkVerificationError::ProofTooLarge {
750                size,
751                max: MAX_STARK_PROOF_BYTES,
752            } if size == MAX_STARK_PROOF_BYTES + 1
753        ));
754    }
755
756    #[test]
757    fn ordered_root_coverage_reaches_vm_stark_verification() {
758        let vm_root = root(2);
759        let proof = complete(vm_root, Some(vec![root(1), vm_root, root(3)]));
760
761        let error = Verifier::new().verify(&claim(), &proof).unwrap_err();
762        assert!(matches!(error, VerificationError::StarkVerificationError(..)));
763    }
764
765    #[test]
766    fn verifier_requires_compatible_vm_and_pvm_roots() {
767        let proof = complete(TRUE_DIGEST, None);
768        let incompatible_vm = ExecutionProof::from_parts(
769            ExecutionProofCompatibility::new(
770                vec![root(100)],
771                VERIFIER_SUPPORT_V2.accepted_pvm_roots.to_vec(),
772            )
773            .unwrap(),
774            proof.vm().clone(),
775            proof.precompile().clone(),
776        );
777        let incompatible_pvm = ExecutionProof::from_parts(
778            ExecutionProofCompatibility::new(
779                VERIFIER_SUPPORT_V2.accepted_vm_roots.to_vec(),
780                vec![root(200)],
781            )
782            .unwrap(),
783            proof.vm().clone(),
784            proof.precompile().clone(),
785        );
786
787        assert!(matches!(
788            Verifier::new().verify(&claim(), &incompatible_vm),
789            Err(VerificationError::IncompatibleVmVerifier)
790        ));
791        assert!(matches!(
792            Verifier::new().verify(&claim(), &incompatible_pvm),
793            Err(VerificationError::IncompatiblePvmVerifier)
794        ));
795    }
796
797    #[test]
798    fn current_proof_compatibility_excludes_verifier_history() {
799        const OLD_VM_ROOT: Word = Word::new([
800            Felt::new_unchecked(1),
801            Felt::new_unchecked(0),
802            Felt::new_unchecked(0),
803            Felt::new_unchecked(0),
804        ]);
805        const OLD_PVM_ROOT: Word = Word::new([
806            Felt::new_unchecked(2),
807            Felt::new_unchecked(0),
808            Felt::new_unchecked(0),
809            Felt::new_unchecked(0),
810        ]);
811        const SUPPORT: VerifierSupport = VerifierSupport {
812            format: ExecutionProofCompatibility::FORMAT_V2,
813            accepted_vm_roots: &[OLD_VM_ROOT, CURRENT_VM_VERIFIER_ROOT],
814            accepted_pvm_roots: &[OLD_PVM_ROOT, CURRENT_PVM_VERIFIER_ROOT],
815        };
816
817        let proof = complete(TRUE_DIGEST, None);
818
819        assert_eq!(proof.compatibility().vm_verifier_roots(), &[CURRENT_VM_VERIFIER_ROOT]);
820        assert_eq!(proof.compatibility().pvm_verifier_roots(), &[CURRENT_PVM_VERIFIER_ROOT]);
821
822        let old_compatible = ExecutionProof::from_parts(
823            ExecutionProofCompatibility::new(vec![OLD_VM_ROOT], vec![OLD_PVM_ROOT]).unwrap(),
824            proof.vm().clone(),
825            proof.precompile().clone(),
826        );
827        assert!(SUPPORT.check(&old_compatible).is_ok());
828    }
829}