sp1_recursion_circuit_v2/machine/core.rs
1use std::{
2 array,
3 borrow::{Borrow, BorrowMut},
4 marker::PhantomData,
5};
6
7use itertools::Itertools;
8use p3_baby_bear::BabyBear;
9use p3_commit::Mmcs;
10use p3_field::AbstractField;
11use p3_matrix::dense::RowMajorMatrix;
12use sp1_core_machine::riscv::RiscvAir;
13use sp1_primitives::consts::WORD_SIZE;
14use sp1_recursion_core_v2::air::PV_DIGEST_NUM_WORDS;
15use sp1_stark::{
16 air::{PublicValues, POSEIDON_NUM_WORDS},
17 StarkMachine, Word,
18};
19
20use crate::{
21 utils::commit_recursion_public_values, BabyBearFriConfig, BabyBearFriConfigVariable,
22 CircuitConfig,
23};
24
25use sp1_recursion_compiler::{
26 circuit::CircuitV2Builder,
27 ir::{Builder, Config, Ext, ExtConst, Felt},
28};
29
30use sp1_recursion_core_v2::{
31 air::{RecursionPublicValues, RECURSIVE_PROOF_NUM_PV_ELTS},
32 DIGEST_SIZE,
33};
34
35use crate::{
36 challenger::{CanObserveVariable, DuplexChallengerVariable},
37 stark::{ShardProofVariable, StarkVerifier},
38 VerifyingKeyVariable,
39};
40
41pub struct SP1RecursionWitnessVariable<
42 C: CircuitConfig<F = BabyBear>,
43 SC: BabyBearFriConfigVariable<C>,
44> {
45 pub vk: VerifyingKeyVariable<C, SC>,
46 pub shard_proofs: Vec<ShardProofVariable<C, SC>>,
47 pub leaf_challenger: SC::FriChallengerVariable,
48 pub initial_reconstruct_challenger: DuplexChallengerVariable<C>,
49 pub is_complete: Felt<C::F>,
50}
51
52/// A program for recursively verifying a batch of SP1 proofs.
53#[derive(Debug, Clone, Copy)]
54pub struct SP1RecursiveVerifier<C: Config, SC: BabyBearFriConfig> {
55 _phantom: PhantomData<(C, SC)>,
56}
57
58impl<C, SC> SP1RecursiveVerifier<C, SC>
59where
60 SC: BabyBearFriConfigVariable<
61 C,
62 FriChallengerVariable = DuplexChallengerVariable<C>,
63 Digest = [Felt<BabyBear>; DIGEST_SIZE],
64 >,
65 C: CircuitConfig<F = SC::Val, EF = SC::Challenge, Bit = Felt<BabyBear>>,
66 <SC::ValMmcs as Mmcs<BabyBear>>::ProverData<RowMajorMatrix<BabyBear>>: Clone,
67{
68 /// Verify a batch of SP1 shard proofs and aggregate their public values.
69 ///
70 /// This program represents a first recursive step in the verification of an SP1 proof
71 /// consisting of one or more shards. Each shard proof is verified and its public values are
72 /// aggregated into a single set representing the start and end state of the program execution
73 /// across all shards.
74 ///
75 /// # Constraints
76 ///
77 /// ## Verifying the STARK proofs.
78 /// For each shard, the verifier asserts the correctness of the STARK proof which is composed
79 /// of verifying the FRI proof for openings and verifying the constraints.
80 ///
81 /// ## Aggregating the shard public values.
82 /// See [SP1Prover::verify] for the verification algorithm of a complete SP1 proof. In this
83 /// function, we are aggregating several shard proofs and attesting to an aggregated state which
84 /// represents all the shards.
85 ///
86 /// ## The leaf challenger.
87 /// A key difference between the recursive tree verification and the complete one in
88 /// [SP1Prover::verify] is that the recursive verifier has no way of reconstructing the
89 /// chanllenger only from a part of the shard proof. Therefore, the value of the leaf challenger
90 /// is witnessed in the program and the verifier asserts correctness given this challenger.
91 /// In the course of the recursive verification, the challenger is reconstructed by observing
92 /// the commitments one by one, and in the final step, the challenger is asserted to be the same
93 /// as the one witnessed here.
94 pub fn verify(
95 builder: &mut Builder<C>,
96 machine: &StarkMachine<SC, RiscvAir<SC::Val>>,
97 input: SP1RecursionWitnessVariable<C, SC>,
98 ) {
99 // Read input.
100 let SP1RecursionWitnessVariable {
101 vk,
102 shard_proofs,
103 leaf_challenger,
104 initial_reconstruct_challenger,
105 is_complete,
106 } = input;
107
108 // Initialize shard variables.
109 let initial_shard: Felt<_> = builder.uninit();
110 let current_shard: Felt<_> = builder.uninit();
111
112 // Initialize execution shard variables.
113 let initial_execution_shard: Felt<_> = builder.uninit();
114 let current_execution_shard: Felt<_> = builder.uninit();
115
116 // Initialize program counter variables.
117 let start_pc: Felt<_> = builder.uninit();
118 let current_pc: Felt<_> = builder.uninit();
119
120 // Initialize memory initialization and finalization variables.
121 let initial_previous_init_addr_bits: [Felt<_>; 32] = array::from_fn(|_| builder.uninit());
122 let initial_previous_finalize_addr_bits: [Felt<_>; 32] =
123 array::from_fn(|_| builder.uninit());
124 let current_init_addr_bits: [Felt<_>; 32] = array::from_fn(|_| builder.uninit());
125 let current_finalize_addr_bits: [Felt<_>; 32] = array::from_fn(|_| builder.uninit());
126
127 // Initialize the exit code variable.
128 let exit_code: Felt<_> = builder.uninit();
129
130 // Initialize the public values digest.
131 let committed_value_digest: [Word<Felt<_>>; PV_DIGEST_NUM_WORDS] =
132 array::from_fn(|_| Word(array::from_fn(|_| builder.uninit())));
133
134 // Initialize the deferred proofs digest.
135 let deferred_proofs_digest: [Felt<_>; POSEIDON_NUM_WORDS] =
136 array::from_fn(|_| builder.uninit());
137
138 // Initialize the challenger variables.
139 let leaf_challenger_public_values = leaf_challenger.public_values(builder);
140 let mut reconstruct_challenger: DuplexChallengerVariable<_> =
141 initial_reconstruct_challenger.copy(builder);
142
143 // Initialize the cumulative sum.
144 let cumulative_sum: Ext<_, _> = builder.eval(C::EF::zero().cons());
145
146 // Assert that the number of proofs is not zero.
147 assert!(!shard_proofs.is_empty());
148
149 // Verify proofs.
150 for (i, shard_proof) in shard_proofs.into_iter().enumerate() {
151 let contains_cpu = shard_proof.contains_cpu();
152 let _contains_memory_init = shard_proof.contains_memory_init();
153 let _contains_memory_finalize = shard_proof.contains_memory_finalize();
154
155 // Get the public values.
156 let public_values: &PublicValues<Word<Felt<_>>, Felt<_>> =
157 shard_proof.public_values.as_slice().borrow();
158
159 let _shard = public_values.shard;
160
161 // If this is the first proof in the batch, initialize the variables.
162 if i == 0 {
163 // Shard.
164 builder.assign(initial_shard, public_values.shard);
165 builder.assign(current_shard, public_values.shard);
166
167 // Execution shard.
168 builder.assign(initial_execution_shard, public_values.execution_shard);
169 builder.assign(current_execution_shard, public_values.execution_shard);
170
171 // Program counter.
172 builder.assign(start_pc, public_values.start_pc);
173 builder.assign(current_pc, public_values.start_pc);
174
175 // Memory initialization & finalization.
176 for ((bit, pub_bit), first_bit) in current_init_addr_bits
177 .iter()
178 .zip(public_values.previous_init_addr_bits.iter())
179 .zip(initial_previous_init_addr_bits.iter())
180 {
181 builder.assign(*bit, *pub_bit);
182 builder.assign(*first_bit, *pub_bit);
183 }
184 for ((bit, pub_bit), first_bit) in current_finalize_addr_bits
185 .iter()
186 .zip(public_values.previous_finalize_addr_bits.iter())
187 .zip(initial_previous_finalize_addr_bits.iter())
188 {
189 builder.assign(*bit, *pub_bit);
190 builder.assign(*first_bit, *pub_bit);
191 }
192
193 // Exit code.
194 builder.assign(exit_code, public_values.exit_code);
195
196 // Commited public values digests.
197 for (word, first_word) in committed_value_digest
198 .iter()
199 .zip_eq(public_values.committed_value_digest.iter())
200 {
201 for (byte, first_byte) in word.0.iter().zip_eq(first_word.0.iter()) {
202 builder.assign(*byte, *first_byte);
203 }
204 }
205
206 // Deferred proofs digests.
207 for (digest, first_digest) in deferred_proofs_digest
208 .iter()
209 .zip_eq(public_values.deferred_proofs_digest.iter())
210 {
211 builder.assign(*digest, *first_digest);
212 }
213 }
214
215 // // If the shard is the first shard, assert that the initial challenger is equal to a
216 // // fresh challenger observing the verifier key and the initial pc.
217 // let shard = felt2var(builder, public_values.shard);
218 // builder.if_eq(shard, C::N::one()).then(|builder| {
219 // let mut first_initial_challenger = DuplexChallengerVariable::new(builder);
220 // first_initial_challenger.observe(builder, vk.commitment.clone());
221 // first_initial_challenger.observe(builder, vk.pc_start);
222 // initial_reconstruct_challenger.assert_eq(builder, &first_initial_challenger);
223 // });
224
225 // Verify the shard.
226 //
227 // Do not verify the cumulative sum here, since the permutation challenge is shared
228 // between all shards.
229 let mut challenger = leaf_challenger.copy(builder);
230 StarkVerifier::<C, SC, _>::verify_shard(
231 builder,
232 &vk,
233 machine,
234 &mut challenger,
235 &shard_proof,
236 );
237
238 // // First shard has a "CPU" constraint.
239 // {
240 // builder.if_eq(shard, C::N::one()).then(|builder| {
241 // builder.assert_var_eq(contains_cpu, C::N::one());
242 // });
243 // }
244
245 // // CPU log degree bound check constraints.
246 // {
247 // for (i, chip) in machine.chips().iter().enumerate() {
248 // if chip.name() == "CPU" {
249 // builder.if_eq(contains_cpu, C::N::one()).then(|builder| {
250 // let index = builder.get(&proof.sorted_idxs, i);
251 // let cpu_log_degree =
252 // builder.get(&proof.opened_values.chips, index).log_degree;
253 // let cpu_log_degree_lt_max: Var<_> = builder.eval(C::N::zero());
254 // builder
255 // .range(0, MAX_CPU_LOG_DEGREE + 1)
256 // .for_each(|j, builder| {
257 // builder.if_eq(j, cpu_log_degree).then(|builder| {
258 // builder.assign(cpu_log_degree_lt_max, C::N::one());
259 // });
260 // });
261 // builder.assert_var_eq(cpu_log_degree_lt_max, C::N::one());
262 // });
263 // }
264 // }
265 // }
266
267 // Shard constraints.
268 {
269 // Assert that the shard of the proof is equal to the current shard.
270 builder.assert_felt_eq(current_shard, public_values.shard);
271
272 // Increment the current shard by one.
273 builder.assign(current_shard, current_shard + C::F::one());
274 }
275
276 // Execution shard constraints.
277 // let execution_shard = felt2var(builder, public_values.execution_shard);
278 {
279 // If the shard has a "CPU" chip, then the execution shard should be incremented by
280 // 1.
281 if contains_cpu {
282 // Assert that the shard of the proof is equal to the current shard.
283 // builder.assert_felt_eq(current_execution_shard,
284 // public_values.execution_shard);
285
286 builder.assign(current_execution_shard, current_execution_shard + C::F::one());
287 }
288 }
289
290 // Program counter constraints.
291 {
292 // // If it's the first shard (which is the first execution shard), then the
293 // start_pc // should be vk.pc_start.
294 // builder.if_eq(shard, C::N::one()).then(|builder| {
295 // builder.assert_felt_eq(public_values.start_pc, vk.pc_start);
296 // });
297
298 // // Assert that the start_pc of the proof is equal to the current pc.
299 // builder.assert_felt_eq(current_pc, public_values.start_pc);
300
301 // // If it's not a shard with "CPU", then assert that the start_pc equals the
302 // next_pc. builder.if_ne(contains_cpu, C::N::one()).then(|builder|
303 // { builder.assert_felt_eq(public_values.start_pc,
304 // public_values.next_pc); });
305
306 // // If it's a shard with "CPU", then assert that the start_pc is not zero.
307 // builder.if_eq(contains_cpu, C::N::one()).then(|builder| {
308 // builder.assert_felt_ne(public_values.start_pc, C::F::zero());
309 // });
310
311 // Update current_pc to be the end_pc of the current proof.
312 builder.assign(current_pc, public_values.next_pc);
313 }
314
315 // Exit code constraints.
316 {
317 // Assert that the exit code is zero (success) for all proofs.
318 builder.assert_felt_eq(exit_code, C::F::zero());
319 }
320
321 // Memory initialization & finalization constraints.
322 {
323 // // Assert that `init_addr_bits` and `finalize_addr_bits` are zero for the first
324 // execution shard. builder.if_eq(execution_shard,
325 // C::N::one()).then(|builder| { // Assert that the
326 // MemoryInitialize address bits are zero. for bit in
327 // current_init_addr_bits.iter() { builder.assert_felt_eq(*
328 // bit, C::F::zero()); }
329
330 // // Assert that the MemoryFinalize address bits are zero.
331 // for bit in current_finalize_addr_bits.iter() {
332 // builder.assert_felt_eq(*bit, C::F::zero());
333 // }
334 // });
335
336 // // Assert that the MemoryInitialize address bits match the current loop variable.
337 // for (bit, current_bit) in current_init_addr_bits
338 // .iter()
339 // .zip_eq(public_values.previous_init_addr_bits.iter())
340 // {
341 // builder.assert_felt_eq(*bit, *current_bit);
342 // }
343
344 // // Assert that the MemoryFinalize address bits match the current loop variable.
345 // for (bit, current_bit) in current_finalize_addr_bits
346 // .iter()
347 // .zip_eq(public_values.previous_finalize_addr_bits.iter())
348 // {
349 // builder.assert_felt_eq(*bit, *current_bit);
350 // }
351
352 // // Assert that if MemoryInit is not present, then the address bits are the same.
353 // builder
354 // .if_ne(contains_memory_init, C::N::one())
355 // .then(|builder| {
356 // for (prev_bit, last_bit) in public_values
357 // .previous_init_addr_bits
358 // .iter()
359 // .zip_eq(public_values.last_init_addr_bits.iter())
360 // {
361 // builder.assert_felt_eq(*prev_bit, *last_bit);
362 // }
363 // });
364
365 // // Assert that if MemoryFinalize is not present, then the address bits are the
366 // same. builder
367 // .if_ne(contains_memory_finalize, C::N::one())
368 // .then(|builder| {
369 // for (prev_bit, last_bit) in public_values
370 // .previous_finalize_addr_bits
371 // .iter()
372 // .zip_eq(public_values.last_finalize_addr_bits.iter())
373 // {
374 // builder.assert_felt_eq(*prev_bit, *last_bit);
375 // }
376 // });
377
378 // Update the MemoryInitialize address bits.
379 for (bit, pub_bit) in
380 current_init_addr_bits.iter().zip(public_values.last_init_addr_bits.iter())
381 {
382 builder.assign(*bit, *pub_bit);
383 }
384
385 // Update the MemoryFinalize address bits.
386 for (bit, pub_bit) in current_finalize_addr_bits
387 .iter()
388 .zip(public_values.last_finalize_addr_bits.iter())
389 {
390 builder.assign(*bit, *pub_bit);
391 }
392 }
393
394 // Digest constraints.
395 {
396 // // If `commited_value_digest` is not zero, then
397 // `public_values.commited_value_digest // should be the current
398 // value. let is_zero: Var<_> = builder.eval(C::N::one());
399 // #[allow(clippy::needless_range_loop)]
400 // for i in 0..committed_value_digest.len() {
401 // for j in 0..WORD_SIZE {
402 // let d = felt2var(builder, committed_value_digest[i][j]);
403 // builder.if_ne(d, C::N::zero()).then(|builder| {
404 // builder.assign(is_zero, C::N::zero());
405 // });
406 // }
407 // }
408 // builder.if_eq(is_zero, C::N::zero()).then(|builder| {
409 // #[allow(clippy::needless_range_loop)]
410 // for i in 0..committed_value_digest.len() {
411 // for j in 0..WORD_SIZE {
412 // builder.assert_felt_eq(
413 // committed_value_digest[i][j],
414 // public_values.committed_value_digest[i][j],
415 // );
416 // }
417 // }
418 // });
419
420 // // If it's not a shard with "CPU", then the committed value digest should not
421 // change. builder.if_ne(contains_cpu, C::N::one()).then(|builder| {
422 // #[allow(clippy::needless_range_loop)]
423 // for i in 0..committed_value_digest.len() {
424 // for j in 0..WORD_SIZE {
425 // builder.assert_felt_eq(
426 // committed_value_digest[i][j],
427 // public_values.committed_value_digest[i][j],
428 // );
429 // }
430 // }
431 // });
432
433 // Update the committed value digest.
434 #[allow(clippy::needless_range_loop)]
435 for i in 0..committed_value_digest.len() {
436 for j in 0..WORD_SIZE {
437 builder.assign(
438 committed_value_digest[i][j],
439 public_values.committed_value_digest[i][j],
440 );
441 }
442 }
443
444 // // If `deferred_proofs_digest` is not zero, then
445 // `public_values.deferred_proofs_digest // should be the current
446 // value. let is_zero: Var<_> = builder.eval(C::N::one());
447 // #[allow(clippy::needless_range_loop)]
448 // for i in 0..deferred_proofs_digest.len() {
449 // let d = felt2var(builder, deferred_proofs_digest[i]);
450 // builder.if_ne(d, C::N::zero()).then(|builder| {
451 // builder.assign(is_zero, C::N::zero());
452 // });
453 // }
454 // builder.if_eq(is_zero, C::N::zero()).then(|builder| {
455 // #[allow(clippy::needless_range_loop)]
456 // for i in 0..deferred_proofs_digest.len() {
457 // builder.assert_felt_eq(
458 // deferred_proofs_digest[i],
459 // public_values.deferred_proofs_digest[i],
460 // );
461 // }
462 // });
463
464 // // If it's not a shard with "CPU", then the deferred proofs digest should not
465 // change. builder.if_ne(contains_cpu, C::N::one()).then(|builder| {
466 // #[allow(clippy::needless_range_loop)]
467 // for i in 0..deferred_proofs_digest.len() {
468 // builder.assert_felt_eq(
469 // deferred_proofs_digest[i],
470 // public_values.deferred_proofs_digest[i],
471 // );
472 // }
473 // });
474
475 // Update the deferred proofs digest.
476 #[allow(clippy::needless_range_loop)]
477 for i in 0..deferred_proofs_digest.len() {
478 builder
479 .assign(deferred_proofs_digest[i], public_values.deferred_proofs_digest[i]);
480 }
481 }
482
483 // // Verify that the number of shards is not too large.
484 // builder.range_check_f(public_values.shard, 16);
485
486 // Update the reconstruct challenger.
487 reconstruct_challenger.observe(builder, shard_proof.commitment.main_commit);
488 for element in shard_proof.public_values.iter() {
489 reconstruct_challenger.observe(builder, *element);
490 }
491
492 // Cumulative sum is updated by sums of all chips.
493 for values in shard_proof.opened_values.chips.iter() {
494 builder.assign(cumulative_sum, cumulative_sum + values.cumulative_sum);
495 }
496 }
497
498 // Write all values to the public values struct and commit to them.
499 {
500 // Compute the vk digest.
501 let vk_digest = vk.hash(builder);
502
503 // Collect the public values for challengers.
504 let initial_challenger_public_values =
505 initial_reconstruct_challenger.public_values(builder);
506 let final_challenger_public_values = reconstruct_challenger.public_values(builder);
507
508 // Collect the cumulative sum.
509 let cumulative_sum_array = builder.ext2felt_v2(cumulative_sum);
510
511 // Collect the deferred proof digests.
512 let zero: Felt<_> = builder.eval(C::F::zero());
513 let start_deferred_digest = [zero; POSEIDON_NUM_WORDS];
514 let end_deferred_digest = [zero; POSEIDON_NUM_WORDS];
515
516 // Collect the is_complete flag.
517 // let is_complete_felt = var2felt(builder, is_complete);
518
519 // Initialize the public values we will commit to.
520 let mut recursion_public_values_stream = [zero; RECURSIVE_PROOF_NUM_PV_ELTS];
521 let recursion_public_values: &mut RecursionPublicValues<_> =
522 recursion_public_values_stream.as_mut_slice().borrow_mut();
523 recursion_public_values.committed_value_digest = committed_value_digest;
524 recursion_public_values.deferred_proofs_digest = deferred_proofs_digest;
525 recursion_public_values.start_pc = start_pc;
526 recursion_public_values.next_pc = current_pc;
527 recursion_public_values.start_shard = initial_shard;
528 recursion_public_values.next_shard = current_shard;
529 recursion_public_values.start_execution_shard = initial_execution_shard;
530 recursion_public_values.next_execution_shard = current_execution_shard;
531 recursion_public_values.previous_init_addr_bits = initial_previous_init_addr_bits;
532 recursion_public_values.last_init_addr_bits = current_init_addr_bits;
533 recursion_public_values.previous_finalize_addr_bits =
534 initial_previous_finalize_addr_bits;
535 recursion_public_values.last_finalize_addr_bits = current_finalize_addr_bits;
536 recursion_public_values.sp1_vk_digest = vk_digest;
537 recursion_public_values.leaf_challenger = leaf_challenger_public_values;
538 recursion_public_values.start_reconstruct_challenger = initial_challenger_public_values;
539 recursion_public_values.end_reconstruct_challenger = final_challenger_public_values;
540 recursion_public_values.cumulative_sum = cumulative_sum_array;
541 recursion_public_values.start_reconstruct_deferred_digest = start_deferred_digest;
542 recursion_public_values.end_reconstruct_deferred_digest = end_deferred_digest;
543 recursion_public_values.exit_code = exit_code;
544 recursion_public_values.is_complete = is_complete;
545
546 // // If the proof represents a complete proof, make completeness assertions.
547 // //
548 // // *Remark*: In this program, this only happends if there is one shard and the
549 // program has // no deferred proofs to verify. However, the completeness
550 // check is independent of these // facts.
551 // builder.if_eq(is_complete, C::N::one()).then(|builder| {
552 // assert_complete(builder, recursion_public_values, &reconstruct_challenger)
553 // });
554
555 commit_recursion_public_values(builder, recursion_public_values);
556 }
557 }
558}