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
23const MAX_STARK_PROOF_BYTES: usize = 64 * 1024 * 1024;
25
26fn 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
40mod 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#[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 pub fn new() -> Self {
82 Self::default()
83 }
84
85 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 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 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#[must_use = "the deferred obligation must be settled or re-exposed, not dropped"]
158#[derive(Debug)]
159pub struct Unsettled(DeferredState);
160
161impl Unsettled {
162 pub fn root(&self) -> Word {
164 self.0.root()
165 }
166
167 pub fn into_state(self) -> DeferredState {
169 self.0
170 }
171}
172
173pub fn verify(proof: ExecutionProof, claim: ExecutionClaim) -> Result<u32, VerificationError> {
185 Verifier::default().verify(proof, claim)
186}
187
188fn 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 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#[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#[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
303fn 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 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}