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, vec::Vec};
9
10use miden_air::{MidenMultiAir, PublicInputs, Statement, config};
11use miden_core::{Felt, field::QuadFelt};
12use miden_crypto::stark::{
13    StarkConfig, VerifierInstance, lmcs::Lmcs, proof::StarkProofData, verifier::VerifierError,
14};
15use serde::de::DeserializeOwned;
16use serde_wincode::SerdeCompat;
17
18const MAX_STARK_PROOF_BYTES: usize = 64 * 1024 * 1024;
19
20// RE-EXPORTS
21// ================================================================================================
22mod exports {
23    pub use miden_core::{
24        Word,
25        precompile::{
26            PrecompileTranscriptState, PrecompileVerificationError, PrecompileVerifierRegistry,
27        },
28        program::{Kernel, ProgramInfo, StackInputs, StackOutputs},
29        proof::{ExecutionProof, HashFunction},
30    };
31    pub mod math {
32        pub use miden_core::Felt;
33    }
34}
35pub use exports::*;
36
37// VERIFIER
38// ================================================================================================
39
40/// Returns the security level of the proof if the specified program was executed correctly against
41/// the specified inputs and outputs.
42///
43/// Specifically, verifies that if a program with the specified `program_hash` is executed against
44/// the provided `stack_inputs` and some secret inputs, the result is equal to the `stack_outputs`.
45///
46/// Stack inputs are expected to be ordered as if they would be pushed onto the stack one by one.
47/// Thus, their expected order on the stack will be the reverse of the order in which they are
48/// provided, and the last value in the `stack_inputs` slice is expected to be the value at the top
49/// of the stack.
50///
51/// Stack outputs are expected to be ordered as if they would be popped off the stack one by one.
52/// Thus, the value at the top of the stack is expected to be in the first position of the
53/// `stack_outputs` slice, and the order of the rest of the output elements will also match the
54/// order on the stack. This is the reverse of the order of the `stack_inputs` slice.
55///
56/// # Errors
57/// Returns an error if:
58/// - The provided proof does not prove a correct execution of the program.
59/// - The proof contains one or more precompile requests. When precompile requests are present, use
60///   [`verify_with_precompiles`] instead with an appropriate [`PrecompileVerifierRegistry`] to
61///   verify the precompile computations.
62pub fn verify(
63    program_info: ProgramInfo,
64    stack_inputs: StackInputs,
65    stack_outputs: StackOutputs,
66    proof: ExecutionProof,
67) -> Result<u32, VerificationError> {
68    let (security_level, _commitment) = verify_with_precompiles(
69        program_info,
70        stack_inputs,
71        stack_outputs,
72        proof,
73        &PrecompileVerifierRegistry::new(),
74    )?;
75    Ok(security_level)
76}
77
78/// Identical to [`verify`], with additional verification of any precompile requests made during the
79/// VM execution. The resulting aggregated precompile commitment is returned, which can be compared
80/// against the commitment computed by the VM.
81///
82/// # Returns
83/// Returns a tuple `(security_level, transcript_state)` where:
84/// - `security_level`: The security level (in bits) of the verified proof.
85/// - `transcript_state`: A [`Word`] containing the rolling commitment to all precompile requests,
86///   computed by recomputing and recording each precompile commitment in a transcript. The state is
87///   itself a complete digest — no separate finalization step is needed.
88///
89/// # Errors
90/// Returns any error produced by [`verify`], as well as any errors resulting from precompile
91/// verification.
92#[tracing::instrument("verify_program", skip_all)]
93pub fn verify_with_precompiles(
94    program_info: ProgramInfo,
95    stack_inputs: StackInputs,
96    stack_outputs: StackOutputs,
97    proof: ExecutionProof,
98    precompile_verifiers: &PrecompileVerifierRegistry,
99) -> Result<(u32, PrecompileTranscriptState), VerificationError> {
100    let security_level = proof.security_level();
101
102    let (hash_fn, proof_bytes, precompile_requests) = proof.into_parts();
103
104    // Recompute the precompile transcript by verifying all precompile requests and recording the
105    // commitments.
106    // If no verifiers were provided (e.g. when this function was called from `verify()`),
107    // but the proof contained requests anyway, returns a `NoVerifierFound` error.
108    let recomputed_transcript = precompile_verifiers
109        .requests_transcript(&precompile_requests)
110        .map_err(VerificationError::PrecompileVerificationError)?;
111    let pc_transcript_state = recomputed_transcript.state();
112
113    verify_stark(
114        program_info,
115        stack_inputs,
116        stack_outputs,
117        pc_transcript_state,
118        hash_fn,
119        proof_bytes,
120    )?;
121
122    Ok((security_level, pc_transcript_state))
123}
124
125// HELPER FUNCTIONS
126// ================================================================================================
127
128fn verify_stark(
129    program_info: ProgramInfo,
130    stack_inputs: StackInputs,
131    stack_outputs: StackOutputs,
132    pc_transcript_state: PrecompileTranscriptState,
133    hash_fn: HashFunction,
134    proof_bytes: Vec<u8>,
135) -> Result<(), VerificationError> {
136    let program_hash = *program_info.program_hash();
137
138    let pub_inputs =
139        PublicInputs::new(program_info, stack_inputs, stack_outputs, pc_transcript_state);
140    let (public_values, kernel_felts) = pub_inputs.to_air_inputs();
141
142    let params = config::pcs_params();
143    match hash_fn {
144        HashFunction::Blake3_256 => {
145            let config = config::blake3_256_config(params);
146            verify_stark_proof(&config, &public_values, &kernel_felts, &proof_bytes)
147        },
148        HashFunction::Rpo256 => {
149            let config = config::rpo_config(params);
150            verify_stark_proof(&config, &public_values, &kernel_felts, &proof_bytes)
151        },
152        HashFunction::Rpx256 => {
153            let config = config::rpx_config(params);
154            verify_stark_proof(&config, &public_values, &kernel_felts, &proof_bytes)
155        },
156        HashFunction::Poseidon2 => {
157            let config = config::poseidon2_config(params);
158            verify_stark_proof(&config, &public_values, &kernel_felts, &proof_bytes)
159        },
160        HashFunction::Keccak => {
161            let config = config::keccak_config(params);
162            verify_stark_proof(&config, &public_values, &kernel_felts, &proof_bytes)
163        },
164    }
165    .map_err(|e| VerificationError::StarkVerificationError(program_hash, Box::new(e)))?;
166
167    Ok(())
168}
169
170// ERRORS
171// ================================================================================================
172
173/// Errors that can occur during proof verification.
174#[derive(Debug, thiserror::Error)]
175pub enum VerificationError {
176    #[error("failed to verify STARK proof for program with hash {0}")]
177    StarkVerificationError(Word, #[source] Box<StarkVerificationError>),
178    #[error("failed to verify precompile calls")]
179    PrecompileVerificationError(#[source] PrecompileVerificationError),
180}
181
182// STARK PROOF VERIFICATION
183// ================================================================================================
184
185/// Errors that can occur during low-level STARK proof verification.
186#[derive(Debug, thiserror::Error)]
187pub enum StarkVerificationError {
188    #[error("failed to deserialize proof: {0}")]
189    Deserialization(#[from] wincode::error::ReadError),
190    #[error("STARK proof is too large: {size} bytes exceeds the {max} byte limit")]
191    ProofTooLarge { size: usize, max: usize },
192    #[error(transparent)]
193    Verifier(#[from] VerifierError),
194}
195
196/// Verifies a multi-AIR STARK proof for the given (Core, Chiplets) split.
197///
198/// Pre-seeds the challenger with the protocol parameters, public values, and the
199/// concatenated kernel-procedure digests (the only variable-length public input today,
200/// owned by the Chiplets AIR). Then delegates to the lifted multi-AIR verifier.
201fn verify_stark_proof<SC>(
202    config: &SC,
203    public_values: &[Felt],
204    kernel_felts: &[Felt],
205    proof_bytes: &[u8],
206) -> Result<(), StarkVerificationError>
207where
208    SC: StarkConfig<Felt, QuadFelt>,
209    <SC::Lmcs as Lmcs>::Commitment: DeserializeOwned,
210{
211    if proof_bytes.len() > MAX_STARK_PROOF_BYTES {
212        return Err(StarkVerificationError::ProofTooLarge {
213            size: proof_bytes.len(),
214            max: MAX_STARK_PROOF_BYTES,
215        });
216    }
217
218    let proof_encoding_config = wincode::config::Configuration::default()
219        .with_preallocation_size_limit::<MAX_STARK_PROOF_BYTES>();
220    let proof: StarkProofData<Felt, QuadFelt, SC> = <SerdeCompat<
221        StarkProofData<Felt, QuadFelt, SC>,
222    > as wincode::config::Deserialize<_>>::deserialize(
223        proof_bytes, proof_encoding_config
224    )?;
225
226    let mut challenger = config.challenger();
227    config::observe_protocol_params(&mut challenger);
228
229    // `air_inputs` are the fixed public values; `aux_inputs` are the kernel-procedure
230    // digests. The lifted verifier absorbs both into Fiat-Shamir internally, and derives
231    // the multi-AIR ordering deterministically from the proof's per-AIR trace heights.
232    let statement = Statement::<Felt, QuadFelt, _>::new(
233        MidenMultiAir::new(),
234        public_values.to_vec(),
235        kernel_felts.to_vec(),
236    )
237    .map_err(|e| StarkVerificationError::Verifier(VerifierError::from(e)))?;
238
239    VerifierInstance::new(config, &statement, None)
240        .expect("Miden AIRs declare no preprocessed columns")
241        .verify(&proof, challenger)?;
242    Ok(())
243}
244
245#[cfg(test)]
246mod tests {
247    use alloc::vec::Vec;
248
249    use super::*;
250
251    #[test]
252    fn proof_encoding_config_rejects_oversized_native_vec_preallocation() {
253        let proof_encoding_config = wincode::config::Configuration::default()
254            .with_preallocation_size_limit::<MAX_STARK_PROOF_BYTES>();
255        let element_count = MAX_STARK_PROOF_BYTES + 1;
256        let mut length_prefix = Vec::new();
257
258        <usize as wincode::config::Serialize<_>>::serialize_into(
259            &mut length_prefix,
260            &element_count,
261            proof_encoding_config,
262        )
263        .unwrap();
264        let err = <Vec<u8> as wincode::config::Deserialize<_>>::deserialize(
265            &length_prefix,
266            proof_encoding_config,
267        )
268        .unwrap_err();
269
270        assert!(
271            matches!(
272                err,
273                wincode::error::ReadError::PreallocationSizeLimit { needed, limit }
274                    if needed == element_count && limit == MAX_STARK_PROOF_BYTES
275            ),
276            "expected proof encoding config to reject oversized allocation, got {err:?}"
277        );
278    }
279
280    #[test]
281    fn verify_stark_proof_rejects_oversized_proof_bytes() {
282        let params = config::pcs_params();
283        let config = config::poseidon2_config(params);
284        let proof_bytes = Vec::from_iter(core::iter::repeat_n(0, MAX_STARK_PROOF_BYTES + 1));
285
286        let err = verify_stark_proof(&config, &[], &[], &proof_bytes).unwrap_err();
287
288        assert!(
289            matches!(
290                err,
291                StarkVerificationError::ProofTooLarge {
292                    size,
293                    max: MAX_STARK_PROOF_BYTES,
294                } if size == proof_bytes.len()
295            ),
296            "expected explicit proof byte limit to reject oversized proof, got {err:?}"
297        );
298    }
299}