1use alloc::{
25 string::{String, ToString},
26 sync::Arc,
27 vec::Vec,
28};
29
30use miden_air::{
31 MIDEN_AIR_COUNT, MidenMultiAir, ProofOrder, PublicInputs, Statement,
32 ace::build_recursive_verifier_ace_circuit, config,
33};
34use miden_core::{
35 Felt, Word,
36 advice::AdviceInputs,
37 crypto::merkle::{MerklePath, MerkleStore, PartialMerkleTree},
38 deferred::{DEFAULT_MAX_DEFERRED_ELEMENTS, DeferredState, IntegrityError, TRUE_DIGEST},
39 field::QuadFelt,
40 program::{ExecutionClaim, proof_request_key},
41 proof::{DeferredProof, ExecutionProof, HashFunction},
42};
43use miden_crypto::{
44 field::BasedVectorSpace,
45 stark::{
46 StarkConfig, VerifierInstance,
47 lmcs::{Lmcs, proof::BatchProofView},
48 pcs::{PcsParams, PcsProof},
49 proof::{StarkProof, StarkProofData},
50 verifier::VerifierError as CryptoVerifierError,
51 },
52};
53use miden_serde_utils::deserialize_schema_exact;
54use serde_wincode::{SerdeCompat, wincode};
55
56use crate::MAX_STARK_PROOF_BYTES;
57
58type Challenge = QuadFelt;
62type P2Config = config::Poseidon2Config;
63type P2Lmcs = <P2Config as StarkConfig<Felt, Challenge>>::Lmcs;
64type P2ProofData = StarkProofData<Felt, Challenge, P2Config>;
65
66#[derive(Debug, Clone, Eq, PartialEq)]
71pub struct RecursiveVerifierInputs {
72 advice: AdviceInputs,
73 claim_commitment: Word,
74}
75
76impl RecursiveVerifierInputs {
77 pub fn for_request(
87 verifier_root: Word,
88 proof: &ExecutionProof,
89 claim: &ExecutionClaim,
90 ) -> Result<Self, RecursiveVerifierInputsError> {
91 Ok(build_verifier_inputs(proof, claim)?.into_request_package(verifier_root))
92 }
93
94 pub fn advice(&self) -> &AdviceInputs {
96 &self.advice
97 }
98
99 pub fn claim_commitment(&self) -> Word {
101 self.claim_commitment
102 }
103
104 pub fn into_parts(self) -> (AdviceInputs, Word) {
106 (self.advice, self.claim_commitment)
107 }
108
109 fn into_request_package(mut self, verifier_root: Word) -> Self {
112 let key = proof_request_key(verifier_root, self.claim_commitment);
113 let (proof_stream, map, store) = self.advice.into_parts();
114 self.advice = AdviceInputs::default().with_merkle_store(store);
115 self.advice.map = map;
116 self.advice.map.insert(key, proof_stream.into_elements());
117 self
118 }
119}
120
121fn build_verifier_inputs(
123 proof: &ExecutionProof,
124 claim: &ExecutionClaim,
125) -> Result<RecursiveVerifierInputs, RecursiveVerifierInputsError> {
126 let stark = proof.miden_proof();
127 if stark.hash_fn() != HashFunction::Poseidon2 {
128 return Err(RecursiveVerifierInputsError::UnsupportedHashFunction(stark.hash_fn()));
129 }
130 let pub_inputs = PublicInputs::new(
131 claim.to_program_info(),
132 *claim.stack_inputs(),
133 *claim.stack_outputs(),
134 resolve_deferred_root(proof.deferred_proof())?,
135 );
136
137 let claim_commitment = claim.commitment();
138 let mut inputs = build_from_proof_bytes(stark.bytes(), &pub_inputs, claim_commitment)?;
139
140 inputs.advice.map.insert(claim_commitment, claim.to_elements().to_vec());
142
143 let kernel = claim.kernel();
144 let kernel_witness = Word::words_as_elements(kernel.proc_hashes()).to_vec();
146 inputs.advice.map.insert(kernel.commitment(), kernel_witness);
147
148 Ok(inputs)
149}
150
151#[derive(Debug, thiserror::Error)]
153pub enum RecursiveVerifierInputsError {
154 #[error("proof deserialization error: {0}")]
155 ProofDeserialization(String),
156 #[error("STARK proof is too large: {size} bytes exceeds the {max} byte limit")]
157 ProofTooLarge { size: usize, max: usize },
158 #[error("invalid proof shape: {0}")]
159 InvalidProofShape(&'static str),
160 #[error("statement assembly error: {0}")]
161 StatementAssembly(String),
162 #[error("deferred wire hydration failed: {0}")]
163 DeferredIntegrity(#[from] IntegrityError),
164 #[error("recursive verification supports only Poseidon2 proofs, got {0:?}")]
165 UnsupportedHashFunction(HashFunction),
166 #[error("transcript error: {0}")]
167 Transcript(#[from] CryptoVerifierError),
168}
169
170type MerkleAdvice = (MerkleStore, Vec<(Word, Vec<Felt>)>);
172
173struct MidenTraceHeights {
176 instance_log_heights: [usize; MIDEN_AIR_COUNT],
177 proof_order: ProofOrder,
178}
179
180fn resolve_deferred_root(deferred: &DeferredProof) -> Result<Word, RecursiveVerifierInputsError> {
185 match deferred {
186 DeferredProof::Empty => Ok(TRUE_DIGEST),
187 DeferredProof::Stark { public_root, .. } => Ok(*public_root),
188 DeferredProof::Wire(wire) => Ok(DeferredState::from_wire(
189 Arc::new(miden_precompiles::registry()),
190 wire,
191 DEFAULT_MAX_DEFERRED_ELEMENTS,
192 )?
193 .root()),
194 }
195}
196
197fn build_from_proof_bytes(
201 proof_bytes: &[u8],
202 pub_inputs: &PublicInputs,
203 claim_commitment: Word,
204) -> Result<RecursiveVerifierInputs, RecursiveVerifierInputsError> {
205 let config = config::poseidon2_config(config::pcs_params(), config::RELATION_DIGEST);
206
207 let proof = deserialize_proof(proof_bytes)?;
208
209 let (public_values, aux_inputs) = pub_inputs.to_air_inputs();
210 let mut challenger = config.challenger();
211 config::observe_protocol_params(config.pcs(), &mut challenger);
212
213 let statement =
214 Statement::<Felt, Challenge, _>::new(MidenMultiAir::new(), public_values, aux_inputs)
215 .map_err(|e| RecursiveVerifierInputsError::StatementAssembly(e.to_string()))?;
216 let verifier_instance = VerifierInstance::new(&config, &statement, None)
217 .expect("Miden AIRs declare no preprocessed columns");
218
219 let (stark, _digest) = StarkProof::from_data(&verifier_instance, &proof, challenger)?;
220
221 let heights = miden_trace_heights(&stark)?;
222
223 build_advice(&config, &stark, heights, pub_inputs, claim_commitment)
224}
225
226fn deserialize_proof(proof_bytes: &[u8]) -> Result<P2ProofData, RecursiveVerifierInputsError> {
229 if proof_bytes.len() > MAX_STARK_PROOF_BYTES {
230 return Err(RecursiveVerifierInputsError::ProofTooLarge {
231 size: proof_bytes.len(),
232 max: MAX_STARK_PROOF_BYTES,
233 });
234 }
235
236 let encoding_config = wincode::config::Configuration::default()
237 .with_preallocation_size_limit::<MAX_STARK_PROOF_BYTES>();
238 deserialize_schema_exact::<SerdeCompat<P2ProofData>, _>(proof_bytes, encoding_config)
239 .map_err(|e| RecursiveVerifierInputsError::ProofDeserialization(e.to_string()))
240}
241
242fn miden_trace_heights(
243 stark: &StarkProof<Challenge, P2Lmcs>,
244) -> Result<MidenTraceHeights, RecursiveVerifierInputsError> {
245 let log_heights = stark.log_trace_heights();
246 let Ok(log_heights): Result<[u8; MIDEN_AIR_COUNT], _> = log_heights.try_into() else {
247 return Err(RecursiveVerifierInputsError::InvalidProofShape(
248 "unexpected number of AIR log heights",
249 ));
250 };
251 Ok(MidenTraceHeights {
252 instance_log_heights: log_heights.map(usize::from),
253 proof_order: ProofOrder::from_instance_log_heights(&log_heights),
254 })
255}
256
257fn build_advice(
259 config: &P2Config,
260 stark: &StarkProof<Challenge, P2Lmcs>,
261 heights: MidenTraceHeights,
262 pub_inputs: &PublicInputs,
263 claim_commitment: Word,
264) -> Result<RecursiveVerifierInputs, RecursiveVerifierInputsError> {
265 let pcs = &stark.pcs_proof;
266 if stark.all_aux_values.len() != MIDEN_AIR_COUNT {
267 return Err(RecursiveVerifierInputsError::InvalidProofShape(
268 "unexpected number of aux-final groups",
269 ));
270 }
271
272 let mut advice_stack = security_parameter_words(config.pcs()).to_vec();
280
281 advice_stack.extend_from_slice(pub_inputs.deferred_root().as_ref());
283
284 for height in heights.instance_log_heights {
285 advice_stack.push(Felt::new_unchecked(height as u64));
286 }
287
288 advice_stack.extend_from_slice(&commitment_felts(stark.main_commit));
289 advice_stack.extend_from_slice(&commitment_felts(stark.aux_commit));
290
291 for aux_values in &stark.all_aux_values {
292 advice_stack.extend_from_slice(&challenge_felts(aux_values));
293 }
294
295 advice_stack.extend_from_slice(&commitment_felts(stark.quotient_commit));
296
297 let deep_alpha = pcs.deep_proof.challenge_columns;
299 let deep_coeffs: &[Felt] = deep_alpha.as_basis_coefficients_slice();
300 advice_stack.extend_from_slice(&[deep_coeffs[1], deep_coeffs[0]]);
301
302 append_ood_evaluations(&mut advice_stack, pcs)?;
303
304 advice_stack.push(pcs.deep_proof.pow_witness);
305
306 for round in &pcs.fri_proof.rounds {
307 advice_stack.extend_from_slice(&commitment_felts(round.commitment));
308 advice_stack.push(round.pow_witness);
309 }
310
311 let final_poly = &pcs.fri_proof.final_poly;
312 advice_stack.extend_from_slice(&QuadFelt::flatten_to_base(final_poly.to_vec()));
313
314 advice_stack.push(pcs.query_pow_witness);
315
316 let (store, advice_map) = build_merkle_data(config, stark, &heights.proof_order)?;
317
318 let advice = AdviceInputs::default()
319 .with_advice_stack(advice_stack.into())
320 .with_map(advice_map)
321 .with_merkle_store(store);
322
323 Ok(RecursiveVerifierInputs { advice, claim_commitment })
324}
325
326fn security_parameter_words(params: &PcsParams) -> [Felt; 4] {
328 [
329 Felt::new_unchecked(params.num_queries() as u64),
330 Felt::new_unchecked(params.query_pow_bits() as u64),
331 Felt::new_unchecked(params.deep_pow_bits() as u64),
332 Felt::new_unchecked(params.folding_pow_bits() as u64),
333 ]
334}
335
336fn append_ood_evaluations<L>(
344 advice_stack: &mut Vec<Felt>,
345 pcs: &PcsProof<Challenge, L>,
346) -> Result<(), RecursiveVerifierInputsError>
347where
348 L: Lmcs<F = Felt>,
349{
350 let evals = &pcs.deep_proof.evals;
351 let mut local_values = Vec::new();
352 let mut next_values = Vec::new();
353
354 for group in evals {
355 for matrix in group {
356 let width = matrix.width;
357 let values = matrix.values.as_slice();
358 if values.len() != width && values.len() != 2 * width {
360 return Err(RecursiveVerifierInputsError::InvalidProofShape(
361 "OOD matrix must hold exactly one or two rows",
362 ));
363 }
364 local_values.extend_from_slice(&values[..width]);
365 if values.len() == 2 * width {
366 next_values.extend_from_slice(&values[width..]);
367 }
368 }
369 }
370
371 advice_stack.extend_from_slice(&challenge_felts(&local_values));
372 advice_stack.extend_from_slice(&challenge_felts(&next_values));
373 Ok(())
374}
375
376fn build_merkle_data(
385 config: &P2Config,
386 stark: &StarkProof<Challenge, P2Lmcs>,
387 proof_order: &ProofOrder,
388) -> Result<MerkleAdvice, RecursiveVerifierInputsError> {
389 let pcs = &stark.pcs_proof;
390 let lmcs = config.lmcs();
391
392 let mut store = MerkleStore::new();
393 let mut advice_map = Vec::new();
394
395 for batch_proof in pcs.deep_witnesses.iter().chain(pcs.fri_witnesses.iter()) {
398 let (tree, entries) = batch_proof_to_merkle(lmcs, batch_proof)?;
399 store.extend(tree.inner_nodes());
400 advice_map.extend(entries);
401 }
402
403 let registry_tree = config::ace_circuit_registry_tree();
404 store.extend(registry_tree.inner_nodes());
405
406 let circuit = build_recursive_verifier_ace_circuit(proof_order).map_err(|_| {
407 RecursiveVerifierInputsError::InvalidProofShape("failed to build recursive ACE circuit")
408 })?;
409 advice_map.push((circuit.commitment, circuit.instructions));
410
411 Ok((store, advice_map))
412}
413
414fn batch_proof_to_merkle<L>(
417 lmcs: &L,
418 batch_proof: &L::BatchProof,
419) -> Result<(PartialMerkleTree, Vec<(Word, Vec<Felt>)>), RecursiveVerifierInputsError>
420where
421 L: Lmcs<F = Felt>,
422 L::Commitment: Copy + PartialEq + Into<[Felt; 4]>,
423 L::BatchProof: BatchProofView<Felt, L::Commitment>,
424{
425 let mut paths = Vec::new();
426 let mut advice_entries = Vec::new();
427
428 for index in batch_proof.indices() {
429 let rows =
430 batch_proof
431 .opening(index)
432 .ok_or(RecursiveVerifierInputsError::InvalidProofShape(
433 "missing opening for query index",
434 ))?;
435 let siblings = batch_proof.path(index).ok_or(
436 RecursiveVerifierInputsError::InvalidProofShape("missing Merkle path for query index"),
437 )?;
438
439 let leaf_data: Vec<Felt> = rows.as_slice().to_vec();
440 let leaf_word: Word = Word::new(lmcs.hash(rows.iter_rows()).into());
441 let merkle_path =
442 MerklePath::new(siblings.into_iter().map(|c| Word::new(c.into())).collect());
443
444 paths.push((index as u64, leaf_word, merkle_path));
445 advice_entries.push((leaf_word, leaf_data));
446 }
447
448 let tree = PartialMerkleTree::with_paths(paths)
449 .map_err(|_| RecursiveVerifierInputsError::InvalidProofShape("invalid merkle paths"))?;
450
451 Ok((tree, advice_entries))
452}
453
454fn commitment_felts<C: Copy + Into<[Felt; 4]>>(commitment: C) -> [Felt; 4] {
455 commitment.into()
456}
457
458fn challenge_felts(challenges: &[Challenge]) -> Vec<Felt> {
459 QuadFelt::flatten_to_base(challenges.to_vec())
460}
461
462#[cfg(test)]
466mod tests {
467 use alloc::vec;
468
469 use miden_core::{
470 crypto::merkle::InnerNodeInfo,
471 program::{KernelDescriptor, ProgramInfo, StackInputs, StackOutputs},
472 };
473
474 use super::*;
475
476 #[test]
479 fn recursive_verifier_inputs_reject_non_poseidon2_proofs() {
480 let proof = ExecutionProof::from_parts(
481 Vec::new(),
482 HashFunction::Blake3_256,
483 DeferredProof::empty(),
484 );
485 let claim = ExecutionClaim::from_program_info(
486 ProgramInfo::new(Word::default(), KernelDescriptor::default()),
487 StackInputs::default(),
488 StackOutputs::default(),
489 );
490
491 let err = RecursiveVerifierInputs::for_request(Word::default(), &proof, &claim)
492 .expect_err("a Blake3 proof must be rejected");
493 assert!(matches!(
494 err,
495 RecursiveVerifierInputsError::UnsupportedHashFunction(HashFunction::Blake3_256)
496 ));
497 }
498
499 #[test]
502 fn security_parameter_header_uses_the_supplied_pcs_params() {
503 let params = PcsParams::new(4, 3, 6, 5, 11, 19, 13).expect("valid distinct PCS params");
504
505 assert_eq!(security_parameter_words(¶ms), [19, 13, 11, 5].map(Felt::new_unchecked),);
506 }
507
508 #[test]
509 fn proof_deserialization_rejects_oversized_input() {
510 let proof_bytes = vec![0; MAX_STARK_PROOF_BYTES + 1];
511
512 let err = deserialize_proof(&proof_bytes).expect_err("oversized proof must be rejected");
513 assert!(matches!(
514 err,
515 RecursiveVerifierInputsError::ProofTooLarge {
516 size,
517 max: MAX_STARK_PROOF_BYTES,
518 } if size == proof_bytes.len()
519 ));
520 }
521
522 #[test]
526 fn request_package_uses_proof_request_key() {
527 let proof_stream: Vec<Felt> = (1..=8u64).map(Felt::new_unchecked).collect();
528 let claim_commitment = Word::from([11u64, 12, 13, 14].map(Felt::new_unchecked));
529 let verifier_root = Word::from([21u64, 22, 23, 24].map(Felt::new_unchecked));
530 let query_entry = (
531 Word::from([31u64, 32, 33, 34].map(Felt::new_unchecked)),
532 vec![Felt::new_unchecked(7)],
533 );
534 let merkle_node = InnerNodeInfo {
535 value: Word::from([41u64, 42, 43, 44].map(Felt::new_unchecked)),
536 left: Word::from([51u64, 52, 53, 54].map(Felt::new_unchecked)),
537 right: Word::from([61u64, 62, 63, 64].map(Felt::new_unchecked)),
538 };
539 let store: MerkleStore = [merkle_node].into_iter().collect();
540
541 let advice = AdviceInputs::default()
542 .with_advice_stack(proof_stream.clone().into())
543 .with_map([query_entry.clone()])
544 .with_merkle_store(store.clone());
545 let inputs = RecursiveVerifierInputs { advice, claim_commitment };
546
547 let package = inputs.into_request_package(verifier_root);
548
549 assert!(
550 package.advice().advice_stack().is_empty(),
551 "the proof must leave the advice stack"
552 );
553 assert_eq!(package.claim_commitment(), claim_commitment);
554 assert_eq!(&package.advice().store, &store);
555 assert_eq!(package.advice().map.len(), 2, "existing entries stay, proof entry added");
556 assert_eq!(package.advice().map.get(&query_entry.0).unwrap().as_ref(), query_entry.1);
557 assert_eq!(
558 package
559 .advice()
560 .map
561 .get(&proof_request_key(verifier_root, claim_commitment))
562 .unwrap()
563 .as_ref(),
564 proof_stream
565 );
566 }
567}