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};
11use miden_core::{
12    Felt,
13    deferred::{DEFAULT_MAX_DEFERRED_ELEMENTS, TRUE_DIGEST},
14    field::QuadFelt,
15};
16use miden_crypto::stark::{
17    StarkConfig, VerifierInstance, lmcs::Lmcs, proof::StarkProofData, verifier::VerifierError,
18};
19use serde::de::DeserializeOwned;
20use serde_wincode::{SerdeCompat, wincode};
21use wincode::io::Reader as _;
22
23/// Maximum encoded STARK proof size and per-sequence preallocation.
24const MAX_STARK_PROOF_BYTES: usize = 64 * 1024 * 1024;
25
26/// Deserializes a serde-backed value and rejects trailing bytes.
27fn deserialize_serde_exact<'de, T, C>(mut bytes: &'de [u8], _: C) -> wincode::ReadResult<T>
28where
29    C: wincode::config::Config,
30    SerdeCompat<T>: wincode::SchemaRead<'de, C, Dst = T>,
31{
32    let value = <SerdeCompat<T> as wincode::SchemaRead<'de, C>>::get(bytes.by_ref())?;
33    if bytes.is_empty() {
34        Ok(value)
35    } else {
36        Err(wincode::error::trailing_bytes())
37    }
38}
39
40// RE-EXPORTS
41// ================================================================================================
42mod exports {
43    pub use miden_core::{
44        Word,
45        deferred::{DeferredState, IntegrityError},
46        program::{ExecutionClaim, KernelDescriptor, ProgramInfo, StackInputs, StackOutputs},
47        proof::{DeferredProof, ExecutionProof, HashFunction, StarkProof},
48    };
49    pub mod math {
50        pub use miden_core::Felt;
51    }
52}
53pub use exports::*;
54
55pub mod recursive;
56
57// VERIFIER
58// ================================================================================================
59
60/// Configurable verifier for Miden execution proofs.
61///
62/// [`Verifier::verify`] performs final verification and rejects wire-backed partial proofs.
63/// [`Verifier::verify_partial`] accepts wire-backed partial proofs, rehydrates their deferred
64/// state using the standard precompile registry, verifies the Miden VM proof against the hydrated
65/// root, and returns the Miden VM security level with the deferred obligation.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct Verifier {
68    max_deferred_elements: usize,
69}
70
71impl Default for Verifier {
72    fn default() -> Self {
73        Self {
74            max_deferred_elements: DEFAULT_MAX_DEFERRED_ELEMENTS,
75        }
76    }
77}
78
79impl Verifier {
80    /// Creates a verifier with default configuration.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Updates the deferred-state element budget used by [`Self::verify_partial`].
86    pub const fn with_max_deferred_elements(mut self, max_deferred_elements: usize) -> Self {
87        self.max_deferred_elements = max_deferred_elements;
88        self
89    }
90
91    /// Returns the security level of the final proof if it proves a correct execution of the
92    /// given claim.
93    ///
94    /// If the proof contains STARK-backed precompile VM proof material, both the precompile VM
95    /// proof and the Miden VM proof are verified, and the returned security level is the minimum
96    /// of the verified proof security levels. If no precompile claims were produced, only the
97    /// Miden VM proof is verified.
98    ///
99    /// # Errors
100    /// Returns an error if:
101    /// - The provided proof does not prove a correct execution of the claim.
102    /// - The proof carries wire-backed deferred proof material, which is a partial/delegable form.
103    /// - The proof's STARK-backed precompile VM proof, if present, does not verify against its
104    ///   public root.
105    pub fn verify(
106        &self,
107        proof: ExecutionProof,
108        claim: ExecutionClaim,
109    ) -> Result<u32, VerificationError> {
110        let miden_security_level = proof.security_level();
111        let (final_deferred_root, precompile_security_level) =
112            resolve_final_deferred_root(proof.deferred_proof())?;
113
114        verify_stark(claim, final_deferred_root, proof.miden_proof())?;
115
116        Ok(precompile_security_level
117            .map(|level| miden_security_level.min(level))
118            .unwrap_or(miden_security_level))
119    }
120
121    /// Verifies a partial proof and returns its Miden VM security level and hydrated deferred
122    /// state.
123    ///
124    /// Partial verification accepts only wire-backed deferred proof material. The wire is hydrated
125    /// using the standard precompile registry and this verifier's deferred-element budget, then the
126    /// Miden VM STARK proof is verified against the hydrated state's root.
127    ///
128    /// If no budget override was configured with [`Self::with_max_deferred_elements`], partial
129    /// verification uses [`DEFAULT_MAX_DEFERRED_ELEMENTS`].
130    ///
131    /// # Errors
132    /// Returns an error if:
133    /// - The proof is not wire-backed partial proof material.
134    /// - The wire cannot be hydrated under the standard precompile registry and configured budget.
135    /// - The provided proof does not prove a correct execution of the program against the hydrated
136    ///   deferred root.
137    pub fn verify_partial(
138        &self,
139        proof: ExecutionProof,
140        claim: ExecutionClaim,
141    ) -> Result<(u32, Unsettled), VerificationError> {
142        let security_level = proof.security_level();
143        let deferred_state =
144            hydrate_deferred_state(proof.deferred_proof(), self.max_deferred_elements)?;
145
146        verify_stark(claim, deferred_state.root(), proof.miden_proof())?;
147
148        Ok((security_level, Unsettled(deferred_state)))
149    }
150}
151
152/// The obligation a partially verified proof hands back: the hydrated deferred state whose root
153/// the verified statement bound.
154///
155/// It must be settled into a final proof form or re-exposed in the caller's own statement; it
156/// must not be dropped.
157#[must_use = "the deferred obligation must be settled or re-exposed, not dropped"]
158#[derive(Debug)]
159pub struct Unsettled(DeferredState);
160
161impl Unsettled {
162    /// Returns the deferred root bound by the verified statement.
163    pub fn root(&self) -> Word {
164        self.0.root()
165    }
166
167    /// Consumes the obligation into its hydrated deferred state, for settlement or re-exposure.
168    pub fn into_state(self) -> DeferredState {
169        self.0
170    }
171}
172
173/// Returns the security level of the final proof if it proves a correct execution of the given
174/// claim, under the default verifier configuration.
175///
176/// Wire-backed deferred proofs are partial/delegable proof material and are rejected here; use
177/// [`Verifier::verify_partial`] to verify and hydrate wire-backed partial proofs.
178///
179/// # Errors
180/// Returns an error if:
181/// - The provided proof does not prove a correct execution of the claim.
182/// - The proof carries wire-backed deferred proof material, which is a partial/delegable form.
183/// - The proof's STARK-backed deferred proof, if present, does not verify against its public root.
184pub fn verify(proof: ExecutionProof, claim: ExecutionClaim) -> Result<u32, VerificationError> {
185    Verifier::default().verify(proof, claim)
186}
187
188// HELPER FUNCTIONS
189// ================================================================================================
190
191fn resolve_final_deferred_root(
192    deferred_proof: &DeferredProof,
193) -> Result<(Word, Option<u32>), VerificationError> {
194    match deferred_proof {
195        DeferredProof::Empty => Ok((TRUE_DIGEST, None)),
196        DeferredProof::Wire(_) => Err(VerificationError::UnsupportedDeferredProof),
197        DeferredProof::Stark { proof, .. } => {
198            let root = miden_precompiles_prover::verify_deferred(deferred_proof)?;
199            Ok((root, Some(stark_security_level(proof))))
200        },
201    }
202}
203
204fn hydrate_deferred_state(
205    deferred_proof: &DeferredProof,
206    max_deferred_elements: usize,
207) -> Result<DeferredState, VerificationError> {
208    match deferred_proof {
209        DeferredProof::Wire(wire) => Ok(DeferredState::from_wire(
210            Arc::new(miden_precompiles::registry()),
211            wire,
212            max_deferred_elements,
213        )?),
214        DeferredProof::Empty | DeferredProof::Stark { .. } => {
215            Err(VerificationError::UnsupportedDeferredProof)
216        },
217    }
218}
219
220fn stark_security_level(_proof: &StarkProof) -> u32 {
221    // TODO: placeholder for the precompile-VM proof's security level. Blocked on the
222    // precompile-VM security estimator (does not exist yet); wire together with the VM-side
223    // native level via `miden_air::config`. `verify` returns `min(vm_level, this)`, so this must
224    // become real before the composite is trustworthy for deferred proofs.
225    96
226}
227
228fn verify_stark(
229    claim: ExecutionClaim,
230    final_deferred_root: Word,
231    stark_proof: &StarkProof,
232) -> Result<(), VerificationError> {
233    let program_hash = claim.program_root();
234
235    let pub_inputs = PublicInputs::new(
236        claim.to_program_info(),
237        *claim.stack_inputs(),
238        *claim.stack_outputs(),
239        final_deferred_root,
240    );
241    let (public_values, aux_inputs) = pub_inputs.to_air_inputs();
242
243    let hash_fn = stark_proof.hash_fn();
244    let proof_bytes = stark_proof.bytes();
245    let params = config::pcs_params();
246    match hash_fn {
247        HashFunction::Blake3_256 => {
248            let config = config::blake3_256_config(params, config::RELATION_DIGEST);
249            verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
250        },
251        HashFunction::Rpo256 => {
252            let config = config::rpo_config(params, config::RELATION_DIGEST);
253            verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
254        },
255        HashFunction::Rpx256 => {
256            let config = config::rpx_config(params, config::RELATION_DIGEST);
257            verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
258        },
259        HashFunction::Poseidon2 => {
260            let config = config::poseidon2_config(params, config::RELATION_DIGEST);
261            verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
262        },
263        HashFunction::Keccak => {
264            let config = config::keccak_config(params, config::RELATION_DIGEST);
265            verify_stark_proof(&config, &public_values, &aux_inputs, proof_bytes)
266        },
267    }
268    .map_err(|e| VerificationError::StarkVerificationError(program_hash, Box::new(e)))?;
269
270    Ok(())
271}
272
273// ERRORS
274// ================================================================================================
275
276/// Errors that can occur during proof verification.
277#[derive(Debug, thiserror::Error)]
278pub enum VerificationError {
279    #[error("failed to verify STARK proof for program with hash {0}")]
280    StarkVerificationError(Word, #[source] Box<StarkVerificationError>),
281    #[error("deferred-DAG integrity check failed: {0}")]
282    DeferredIntegrity(#[from] IntegrityError),
283    #[error("failed to verify STARK-backed deferred proof: {0}")]
284    DeferredStarkVerification(#[from] miden_precompiles_prover::VerifyError),
285    #[error("deferred proof form is not supported by this verification mode")]
286    UnsupportedDeferredProof,
287}
288
289// STARK PROOF VERIFICATION
290// ================================================================================================
291
292/// Errors that can occur during low-level STARK proof verification.
293#[derive(Debug, thiserror::Error)]
294pub enum StarkVerificationError {
295    #[error("failed to deserialize proof: {0}")]
296    Deserialization(#[from] wincode::error::ReadError),
297    #[error("STARK proof is too large: {size} bytes exceeds the {max} byte limit")]
298    ProofTooLarge { size: usize, max: usize },
299    #[error(transparent)]
300    Verifier(#[from] VerifierError),
301}
302
303/// Verifies a multi-AIR STARK proof for the Miden VM statement.
304///
305/// Pre-seeds the challenger with protocol parameters, AIR public values, and statement
306/// `aux_inputs` (program hash, final deferred root, and kernel-procedure digests). Then delegates
307/// to the lifted multi-AIR verifier.
308fn verify_stark_proof<SC>(
309    config: &SC,
310    public_values: &[Felt],
311    aux_inputs: &[Felt],
312    proof_bytes: &[u8],
313) -> Result<(), StarkVerificationError>
314where
315    SC: StarkConfig<Felt, QuadFelt>,
316    <SC::Lmcs as Lmcs>::Commitment: DeserializeOwned,
317{
318    if proof_bytes.len() > MAX_STARK_PROOF_BYTES {
319        return Err(StarkVerificationError::ProofTooLarge {
320            size: proof_bytes.len(),
321            max: MAX_STARK_PROOF_BYTES,
322        });
323    }
324
325    let proof_encoding_config = wincode::config::Configuration::default()
326        .with_preallocation_size_limit::<MAX_STARK_PROOF_BYTES>();
327    let proof = deserialize_serde_exact::<StarkProofData<Felt, QuadFelt, SC>, _>(
328        proof_bytes,
329        proof_encoding_config,
330    )?;
331
332    let mut challenger = config.challenger();
333    config::observe_protocol_params(config.pcs(), &mut challenger);
334
335    // `air_inputs` are the public values read by the AIRs (stack i/o); `aux_inputs` are the
336    // statement inputs read during observation/boundary correction. The lifted verifier absorbs
337    // both into Fiat-Shamir internally, and derives the multi-AIR ordering deterministically from
338    // the proof's per-AIR trace heights.
339    let statement = Statement::<Felt, QuadFelt, _>::new(
340        MidenMultiAir::new(),
341        public_values.to_vec(),
342        aux_inputs.to_vec(),
343    )
344    .map_err(|e| StarkVerificationError::Verifier(VerifierError::from(e)))?;
345
346    VerifierInstance::new(config, &statement, None)
347        .expect("Miden AIRs declare no preprocessed columns")
348        .verify(&proof, challenger)?;
349    Ok(())
350}
351
352#[cfg(test)]
353mod tests {
354    use alloc::vec::Vec;
355
356    use miden_core::deferred::DeferredStateWire;
357
358    use super::*;
359
360    #[test]
361    fn exact_serde_decoding_rejects_trailing_bytes() {
362        let encoding_config = wincode::config::Configuration::default()
363            .with_preallocation_size_limit::<MAX_STARK_PROOF_BYTES>();
364        let mut encoded =
365            <SerdeCompat<u8> as wincode::config::Serialize<_>>::serialize(&7, encoding_config)
366                .expect("u8 serialization must succeed");
367        encoded.push(0);
368
369        let err = deserialize_serde_exact::<u8, _>(&encoded, encoding_config)
370            .expect_err("trailing bytes must be rejected");
371        assert!(matches!(err, wincode::error::ReadError::TrailingBytes));
372    }
373
374    #[test]
375    fn final_deferred_root_resolution_accepts_empty_rejects_wire_and_verifies_stark() {
376        let (root, security_level) = resolve_final_deferred_root(&DeferredProof::Empty).unwrap();
377        assert_eq!(root, TRUE_DIGEST);
378        assert_eq!(security_level, None);
379
380        let wire = DeferredProof::wire(DeferredStateWire::default());
381        let err = resolve_final_deferred_root(&wire).unwrap_err();
382        assert!(
383            matches!(err, VerificationError::UnsupportedDeferredProof),
384            "expected wire-backed partial proof to be rejected, got {err:?}"
385        );
386
387        let stark = DeferredProof::stark(
388            StarkProof::new(Vec::from([0_u8]), HashFunction::Poseidon2),
389            TRUE_DIGEST,
390        );
391        let err = resolve_final_deferred_root(&stark).unwrap_err();
392        assert!(
393            matches!(err, VerificationError::DeferredStarkVerification(_)),
394            "expected invalid STARK-backed precompile VM proof to be verified and rejected, got {err:?}"
395        );
396    }
397
398    #[test]
399    fn partial_deferred_hydration_accepts_wire_and_rejects_final_forms() {
400        let wire = DeferredStateWire::default();
401        let deferred_proof = DeferredProof::wire(wire.clone());
402        let deferred_state = hydrate_deferred_state(&deferred_proof, DEFAULT_MAX_DEFERRED_ELEMENTS)
403            .expect("empty wire should hydrate under the standard precompile registry");
404
405        assert_eq!(deferred_state.root(), TRUE_DIGEST);
406        assert_eq!(deferred_state.to_wire().unwrap(), wire);
407
408        for final_proof in [
409            DeferredProof::Empty,
410            DeferredProof::stark(
411                StarkProof::new(Vec::from([0_u8]), HashFunction::Poseidon2),
412                TRUE_DIGEST,
413            ),
414        ] {
415            let err =
416                hydrate_deferred_state(&final_proof, DEFAULT_MAX_DEFERRED_ELEMENTS).unwrap_err();
417            assert!(
418                matches!(err, VerificationError::UnsupportedDeferredProof),
419                "expected final proof material to be rejected by partial hydration, got {err:?}"
420            );
421        }
422    }
423
424    #[test]
425    fn proof_encoding_config_rejects_oversized_native_vec_preallocation() {
426        let proof_encoding_config = wincode::config::Configuration::default()
427            .with_preallocation_size_limit::<MAX_STARK_PROOF_BYTES>();
428        let element_count = MAX_STARK_PROOF_BYTES + 1;
429        let mut length_prefix = Vec::new();
430
431        <usize as wincode::config::Serialize<_>>::serialize_into(
432            &mut length_prefix,
433            &element_count,
434            proof_encoding_config,
435        )
436        .unwrap();
437        let err = <Vec<u8> as wincode::config::Deserialize<_>>::deserialize(
438            &length_prefix,
439            proof_encoding_config,
440        )
441        .unwrap_err();
442
443        assert!(
444            matches!(
445                err,
446                wincode::error::ReadError::PreallocationSizeLimit { needed, limit }
447                    if needed == element_count && limit == MAX_STARK_PROOF_BYTES
448            ),
449            "expected proof encoding config to reject oversized allocation, got {err:?}"
450        );
451    }
452
453    #[test]
454    fn verify_stark_proof_rejects_oversized_proof_bytes() {
455        let params = config::pcs_params();
456        let config = config::poseidon2_config(params, config::RELATION_DIGEST);
457        let proof_bytes = Vec::from_iter(core::iter::repeat_n(0, MAX_STARK_PROOF_BYTES + 1));
458
459        let err = verify_stark_proof(&config, &[], &[], &proof_bytes).unwrap_err();
460
461        assert!(
462            matches!(
463                err,
464                StarkVerificationError::ProofTooLarge {
465                    size,
466                    max: MAX_STARK_PROOF_BYTES,
467                } if size == proof_bytes.len()
468            ),
469            "expected explicit proof byte limit to reject oversized proof, got {err:?}"
470        );
471    }
472}