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