1use alloc::string::String;
2use alloc::vec::Vec;
3use alloc::{format, vec};
4use core::fmt::Debug;
5
6use hashbrown::HashMap;
7use p3_air::symbolic::{AirLayout, SymbolicAirBuilder, SymbolicExpressionExt};
8use p3_air::{Air, RowWindow};
9use p3_challenger::{CanObserve, FieldChallenger};
10use p3_commit::{Pcs, PolynomialSpace};
11use p3_field::{Algebra, BasedVectorSpace, PrimeCharacteristicRing};
12use p3_lookup::AirWithLookups;
13use p3_lookup::folder::VerifierConstraintFolderWithLookups;
14use p3_lookup::logup::LogUpGadget;
15use p3_lookup::lookup_traits::{Lookup, LookupGadget};
16use p3_matrix::dense::RowMajorMatrixView;
17use p3_matrix::stack::VerticalPair;
18use p3_uni_stark::{VerificationError, VerifierConstraintFolder, recompose_quotient_from_chunks};
19use p3_util::zip_eq::zip_eq;
20use tracing::{info_span, instrument};
21
22use crate::common::{CommonData, get_perm_challenges};
23use crate::config::{
24 Challenge, Domain, PcsError, StarkGenericConfig as SGC, Val, observe_instance_binding,
25};
26use crate::proof::BatchProof;
27use crate::symbolic::get_log_num_quotient_chunks;
28
29#[instrument(skip_all)]
30pub fn verify_batch<SC, A>(
31 config: &SC,
32 airs: &[A],
33 proof: &BatchProof<SC>,
34 public_values: &[Vec<Val<SC>>],
35 common: &CommonData<SC>,
36) -> Result<(), VerificationError<PcsError<SC>>>
37where
38 SC: SGC,
39 SymbolicExpressionExt<Val<SC>, SC::Challenge>: Algebra<SC::Challenge>,
40 A: Air<SymbolicAirBuilder<Val<SC>, SC::Challenge>>
41 + for<'a> Air<VerifierConstraintFolderWithLookups<'a, SC>>,
42 Challenge<SC>: BasedVectorSpace<Val<SC>>,
43{
44 let lookup_gadget = LogUpGadget::new();
46
47 let BatchProof {
48 commitments,
49 opened_values,
50 opening_proof,
51 global_lookup_data,
52 degree_bits,
53 } = proof;
54
55 let all_lookups = &common.lookups;
56
57 let pcs = config.pcs();
58 let mut challenger = config.initialise_challenger();
59
60 if airs.len() != opened_values.instances.len()
62 || airs.len() != public_values.len()
63 || airs.len() != degree_bits.len()
64 || airs.len() != global_lookup_data.len()
65 {
66 return Err(VerificationError::InvalidProofShape);
67 }
68
69 if (opened_values
73 .instances
74 .iter()
75 .any(|ov| ov.base_opened_values.random.is_some() != SC::Pcs::ZK))
76 || (commitments.random.is_some() != SC::Pcs::ZK)
77 {
78 return Err(VerificationError::RandomizationError);
79 }
80
81 let n_instances = airs.len();
83 challenger.observe_base_as_algebra_element::<Challenge<SC>>(Val::<SC>::from_usize(n_instances));
84
85 let mut preprocessed_widths = Vec::with_capacity(airs.len());
88 let mut log_num_quotient_chunks = Vec::with_capacity(airs.len());
90 let mut num_quotient_chunks = Vec::with_capacity(airs.len());
92
93 for (i, air) in airs.iter().enumerate() {
94 let pre_w = common
95 .preprocessed
96 .as_ref()
97 .and_then(|g| g.instances[i].as_ref().map(|m| m.width))
98 .unwrap_or(0);
99 preprocessed_widths.push(pre_w);
100
101 let layout = AirLayout {
102 preprocessed_width: pre_w,
103 main_width: air.width(),
104 num_public_values: air.num_public_values(),
105 ..Default::default()
106 };
107 let log_num_chunks =
108 info_span!("infer log of constraint degree", air_idx = i).in_scope(|| {
109 get_log_num_quotient_chunks::<Val<SC>, SC::Challenge, A, LogUpGadget>(
110 air,
111 layout,
112 &all_lookups[i],
113 config.is_zk(),
114 &lookup_gadget,
115 )
116 });
117 log_num_quotient_chunks.push(log_num_chunks);
118
119 let n_chunks = 1 << (log_num_chunks + config.is_zk());
120 num_quotient_chunks.push(n_chunks);
121 }
122
123 for (i, air) in airs.iter().enumerate() {
124 let air_width = A::width(air);
125 let inst_opened_vals = &opened_values.instances[i];
126 let inst_base_opened_vals = &inst_opened_vals.base_opened_values;
127
128 if inst_base_opened_vals.trace_local.len() != air_width {
130 return Err(VerificationError::InvalidProofShape);
131 }
132 if !airs[i].main_next_row_columns().is_empty() {
133 if inst_base_opened_vals
134 .trace_next
135 .as_ref()
136 .is_none_or(|v| v.len() != air_width)
137 {
138 return Err(VerificationError::InvalidProofShape);
139 }
140 } else if inst_base_opened_vals.trace_next.is_some() {
141 return Err(VerificationError::InvalidProofShape);
142 }
143
144 let n_chunks = num_quotient_chunks[i];
146 if inst_base_opened_vals.quotient_chunks.len() != n_chunks {
147 return Err(VerificationError::InvalidProofShape);
148 }
149
150 for chunk in &inst_base_opened_vals.quotient_chunks {
151 if chunk.len() != Challenge::<SC>::DIMENSION {
152 return Err(VerificationError::InvalidProofShape);
153 }
154 }
155
156 if inst_opened_vals
158 .base_opened_values
159 .random
160 .as_ref()
161 .is_some_and(|r_comm| r_comm.len() != SC::Challenge::DIMENSION)
162 {
163 return Err(VerificationError::RandomizationError);
164 }
165
166 let pre_w = preprocessed_widths[i];
168 let pre_local_len = inst_base_opened_vals
169 .preprocessed_local
170 .as_ref()
171 .map_or(0, |v| v.len());
172 let pre_next_len = inst_base_opened_vals
173 .preprocessed_next
174 .as_ref()
175 .map_or(0, |v| v.len());
176 if pre_w == 0 {
177 if pre_local_len != 0 || pre_next_len != 0 {
178 return Err(VerificationError::InvalidProofShape);
179 }
180 } else if !airs[i].preprocessed_next_row_columns().is_empty() {
181 if pre_local_len != pre_w || pre_next_len != pre_w {
182 return Err(VerificationError::InvalidProofShape);
183 }
184 } else if pre_local_len != pre_w || pre_next_len != 0 {
185 return Err(VerificationError::InvalidProofShape);
186 }
187
188 let ext_db = degree_bits[i];
190 let base_db = ext_db - config.is_zk();
191 let width = A::width(air);
192 observe_instance_binding::<SC>(&mut challenger, ext_db, base_db, width, n_chunks);
193 }
194
195 challenger.observe(commitments.main.clone());
197 for pv in public_values {
198 challenger.observe_slice(pv);
199 }
200
201 for &pre_w in preprocessed_widths.iter() {
204 challenger.observe_base_as_algebra_element::<Challenge<SC>>(Val::<SC>::from_usize(pre_w));
205 }
206 if let Some(global) = &common.preprocessed {
207 challenger.observe(global.commitment.clone());
208 }
209
210 let is_lookup = commitments.permutation.is_some();
212
213 if is_lookup != all_lookups.iter().any(|c| !c.is_empty()) {
214 return Err(VerificationError::InvalidProofShape);
215 }
216
217 let challenges_per_instance =
219 get_perm_challenges::<SC, LogUpGadget>(&mut challenger, all_lookups, &lookup_gadget);
220
221 if is_lookup {
223 challenger.observe(
224 commitments
225 .permutation
226 .clone()
227 .expect("We checked that the commitment exists"),
228 );
229 for data in global_lookup_data.iter().flatten() {
230 challenger.observe_algebra_element(data.expected_cumulated);
231 }
232 }
233
234 let alpha = challenger.sample_algebra_element();
236
237 challenger.observe(commitments.quotient_chunks.clone());
239
240 if let Some(r_commit) = commitments.random.clone() {
243 challenger.observe(r_commit);
244 }
245
246 let zeta = challenger.sample_algebra_element();
248
249 let mut coms_to_verify = vec![];
251
252 let (trace_domains, ext_trace_domains): (Vec<Domain<SC>>, Vec<Domain<SC>>) = degree_bits
254 .iter()
255 .map(|&ext_db| {
256 let base_db = ext_db - config.is_zk();
257 (
258 pcs.natural_domain_for_degree(1 << base_db),
259 pcs.natural_domain_for_degree(1 << ext_db),
260 )
261 })
262 .unzip();
263
264 if let Some(random_commit) = &commitments.random {
265 coms_to_verify.push((
266 random_commit.clone(),
267 ext_trace_domains
268 .iter()
269 .zip(opened_values.instances.iter())
270 .map(|(domain, inst_opened_vals)| {
271 let random_vals = inst_opened_vals.base_opened_values.random.as_ref().unwrap();
273 (*domain, vec![(zeta, random_vals.clone())])
274 })
275 .collect::<Vec<_>>(),
276 ));
277 }
278
279 let trace_round: Vec<_> = ext_trace_domains
280 .iter()
281 .zip(opened_values.instances.iter())
282 .enumerate()
283 .map(|(i, (ext_dom, inst_opened_vals))| {
284 let mut points = vec![(
285 zeta,
286 inst_opened_vals.base_opened_values.trace_local.clone(),
287 )];
288 if !airs[i].main_next_row_columns().is_empty() {
289 let zeta_next = trace_domains[i]
290 .next_point(zeta)
291 .ok_or(VerificationError::NextPointUnavailable)?;
292 points.push((
293 zeta_next,
294 inst_opened_vals
295 .base_opened_values
296 .trace_next
297 .clone()
298 .expect("checked in shape validation"),
299 ));
300 }
301 Ok((*ext_dom, points))
302 })
303 .collect::<Result<Vec<_>, VerificationError<PcsError<SC>>>>()?;
304 coms_to_verify.push((commitments.main.clone(), trace_round));
305
306 let quotient_domains: Vec<Vec<Domain<SC>>> = (0..degree_bits.len())
309 .map(|i| {
310 let ext_db = degree_bits[i];
311 let log_num_chunks = log_num_quotient_chunks[i];
312 let n_chunks = num_quotient_chunks[i];
313 let ext_dom = ext_trace_domains[i];
314 let qdom = ext_dom.create_disjoint_domain(1 << (ext_db + log_num_chunks));
315 qdom.split_domains(n_chunks)
316 })
317 .collect();
318
319 let randomized_quotient_chunks_domains = quotient_domains
321 .iter()
322 .map(|doms| {
323 doms.iter()
324 .map(|dom| pcs.natural_domain_for_degree(dom.size() << (config.is_zk())))
325 .collect::<Vec<_>>()
326 })
327 .collect::<Vec<_>>();
328
329 let mut qc_round = Vec::new();
331 for (i, domains) in randomized_quotient_chunks_domains.iter().enumerate() {
332 let inst_qcs = &opened_values.instances[i]
333 .base_opened_values
334 .quotient_chunks;
335 if inst_qcs.len() != domains.len() {
336 return Err(VerificationError::InvalidProofShape);
337 }
338 for (d, vals) in zip_eq(
339 domains.iter(),
340 inst_qcs,
341 VerificationError::InvalidProofShape,
342 )? {
343 qc_round.push((*d, vec![(zeta, vals.clone())]));
344 }
345 }
346 coms_to_verify.push((commitments.quotient_chunks.clone(), qc_round));
347
348 if let Some(global) = &common.preprocessed {
351 let mut pre_round = Vec::new();
352
353 for (matrix_index, &inst_idx) in global.matrix_to_instance.iter().enumerate() {
354 let pre_w = preprocessed_widths[inst_idx];
355 if pre_w == 0 {
356 return Err(VerificationError::InvalidProofShape);
357 }
358
359 let inst = &opened_values.instances[inst_idx];
360 let local = inst
361 .base_opened_values
362 .preprocessed_local
363 .as_ref()
364 .ok_or(VerificationError::InvalidProofShape)?;
365
366 let ext_db = degree_bits[inst_idx];
368
369 let meta = global.instances[inst_idx]
370 .as_ref()
371 .ok_or(VerificationError::InvalidProofShape)?;
372 if meta.matrix_index != matrix_index || meta.degree_bits != ext_db {
373 return Err(VerificationError::InvalidProofShape);
374 }
375
376 let meta_db = meta.degree_bits;
377 let pre_domain = pcs.natural_domain_for_degree(1 << meta_db);
378 if !airs[inst_idx].preprocessed_next_row_columns().is_empty() {
379 let next = inst
380 .base_opened_values
381 .preprocessed_next
382 .as_ref()
383 .ok_or(VerificationError::InvalidProofShape)?;
384 let zeta_next_i = trace_domains[inst_idx]
385 .next_point(zeta)
386 .ok_or(VerificationError::NextPointUnavailable)?;
387
388 pre_round.push((
389 pre_domain,
390 vec![(zeta, local.clone()), (zeta_next_i, next.clone())],
391 ));
392 } else {
393 pre_round.push((pre_domain, vec![(zeta, local.clone())]));
394 }
395 }
396
397 coms_to_verify.push((global.commitment.clone(), pre_round));
398 }
399
400 if is_lookup {
401 let permutation_commit = commitments.permutation.clone().unwrap();
402 let mut permutation_round = Vec::new();
403 for (i, (ext_dom, inst_opened_vals)) in ext_trace_domains
404 .iter()
405 .zip(opened_values.instances.iter())
406 .enumerate()
407 {
408 if inst_opened_vals.permutation_local.len() != inst_opened_vals.permutation_next.len() {
409 return Err(VerificationError::InvalidProofShape);
410 }
411 if !inst_opened_vals.permutation_local.is_empty() {
412 let zeta_next = trace_domains[i]
413 .next_point(zeta)
414 .ok_or(VerificationError::NextPointUnavailable)?;
415 permutation_round.push((
416 *ext_dom,
417 vec![
418 (zeta, inst_opened_vals.permutation_local.clone()),
419 (zeta_next, inst_opened_vals.permutation_next.clone()),
420 ],
421 ));
422 }
423 }
424 coms_to_verify.push((permutation_commit, permutation_round));
425 }
426
427 pcs.verify(coms_to_verify, opening_proof, &mut challenger)
429 .map_err(VerificationError::InvalidOpeningArgument)?;
430
431 for (i, air) in airs.iter().enumerate() {
434 let _air_span = info_span!("verify constraints", air_idx = i).entered();
435
436 let qc_domains = "ient_domains[i];
437
438 let quotient = recompose_quotient_from_chunks::<SC>(
440 qc_domains,
441 &opened_values.instances[i]
442 .base_opened_values
443 .quotient_chunks,
444 zeta,
445 );
446
447 let aux_width = all_lookups[i]
451 .iter()
452 .flat_map(|ctx| ctx.columns.iter().cloned())
453 .max()
454 .map(|m| m + 1)
455 .unwrap_or(0);
456
457 let recompose = |flat: &[Challenge<SC>]| -> Vec<Challenge<SC>> {
458 if aux_width == 0 {
459 return vec![];
460 }
461 let ext_degree = Challenge::<SC>::DIMENSION;
462 assert!(
463 flat.len() == aux_width * ext_degree,
464 "flattened permutation opening length ({}) must equal aux_width ({}) * DIMENSION ({})",
465 flat.len(),
466 aux_width,
467 ext_degree
468 );
469 flat.chunks_exact(ext_degree)
472 .map(|coeffs| {
473 coeffs
475 .iter()
476 .enumerate()
477 .map(|(j, &coeff)| {
478 coeff
479 * Challenge::<SC>::ith_basis_element(j)
480 .expect("Basis element should exist")
481 })
482 .sum()
483 })
484 .collect()
485 };
486
487 let perm_local_ext = recompose(&opened_values.instances[i].permutation_local);
488 let perm_next_ext = recompose(&opened_values.instances[i].permutation_next);
489
490 let init_trace_domain = trace_domains[i];
492 let trace_next_zeros;
493 let trace_next_ref = match &opened_values.instances[i].base_opened_values.trace_next {
494 Some(v) => v.as_slice(),
495 None => {
496 trace_next_zeros = vec![SC::Challenge::ZERO; A::width(air)];
497 &trace_next_zeros
498 }
499 };
500 let pre_next_zeros;
501 let pre_next_ref = match &opened_values.instances[i]
502 .base_opened_values
503 .preprocessed_next
504 {
505 Some(v) => v.as_slice(),
506 None => {
507 pre_next_zeros = vec![SC::Challenge::ZERO; preprocessed_widths[i]];
508 &pre_next_zeros
509 }
510 };
511 let perm_vals: Vec<SC::Challenge> = global_lookup_data[i]
512 .iter()
513 .map(|ld| ld.expected_cumulated)
514 .collect();
515 let verifier_data = VerifierData {
516 trace_local: &opened_values.instances[i].base_opened_values.trace_local,
517 trace_next: trace_next_ref,
518 preprocessed_local: opened_values.instances[i]
519 .base_opened_values
520 .preprocessed_local
521 .as_ref()
522 .map_or(&[], |v| v),
523 preprocessed_next: pre_next_ref,
524 permutation_local: &perm_local_ext,
525 permutation_next: &perm_next_ext,
526 permutation_challenges: &challenges_per_instance[i],
527 permutation_values: &perm_vals,
528 lookups: &all_lookups[i],
529 public_values: &public_values[i],
530 trace_domain: init_trace_domain,
531 zeta,
532 alpha,
533 quotient,
534 };
535
536 verify_constraints_with_lookups::<SC, A, LogUpGadget, PcsError<SC>>(
537 air,
538 &verifier_data,
539 &lookup_gadget,
540 )
541 .map_err(|e| match e {
542 VerificationError::OodEvaluationMismatch { .. } => {
543 VerificationError::OodEvaluationMismatch { index: Some(i) }
544 }
545 other => other,
546 })?;
547 }
548
549 let mut global_cumulative = HashMap::<&String, Vec<_>>::new();
550 for data in global_lookup_data.iter().flatten() {
551 global_cumulative
552 .entry(&data.name)
553 .or_default()
554 .push(data.expected_cumulated);
555 }
556
557 for (name, all_expected_cumulative) in global_cumulative {
558 lookup_gadget
559 .verify_global_final_value(&all_expected_cumulative)
560 .map_err(|e| VerificationError::LookupError(format!("{e:?}: {name}")))?;
561 }
562
563 Ok(())
564}
565
566pub struct VerifierData<'a, SC: SGC> {
568 zeta: SC::Challenge,
570 alpha: SC::Challenge,
572 trace_local: &'a [SC::Challenge],
574 trace_next: &'a [SC::Challenge],
576 preprocessed_local: &'a [SC::Challenge],
578 preprocessed_next: &'a [SC::Challenge],
580 permutation_local: &'a [SC::Challenge],
582 permutation_next: &'a [SC::Challenge],
584 permutation_challenges: &'a [SC::Challenge],
586 permutation_values: &'a [SC::Challenge],
588 lookups: &'a [Lookup<Val<SC>>],
590 public_values: &'a [Val<SC>],
592 trace_domain: Domain<SC>,
594 quotient: SC::Challenge,
596}
597
598#[allow(clippy::too_many_arguments)]
603pub fn verify_constraints_with_lookups<'a, SC, A, LG: LookupGadget, PcsErr: Debug>(
604 air: &A,
605 verifier_data: &VerifierData<'a, SC>,
606 lookup_gadget: &LG,
607) -> Result<(), VerificationError<PcsErr>>
608where
609 SC: SGC,
610 A: for<'b> Air<VerifierConstraintFolderWithLookups<'b, SC>>,
611{
612 let VerifierData {
613 trace_local,
614 trace_next,
615 preprocessed_local,
616 preprocessed_next,
617 permutation_local,
618 permutation_next,
619 permutation_challenges,
620 permutation_values,
621 lookups,
622 public_values,
623 trace_domain,
624 zeta,
625 alpha,
626 quotient,
627 } = verifier_data;
628
629 let sels = trace_domain.selectors_at_point(*zeta);
630
631 let main = VerticalPair::new(
632 RowMajorMatrixView::new_row(trace_local),
633 RowMajorMatrixView::new_row(trace_next),
634 );
635
636 let preprocessed = VerticalPair::new(
637 RowMajorMatrixView::new_row(preprocessed_local),
638 RowMajorMatrixView::new_row(preprocessed_next),
639 );
640
641 let preprocessed_window =
642 RowWindow::from_two_rows(preprocessed.top.values, preprocessed.bottom.values);
643 let inner_folder = VerifierConstraintFolder {
644 main,
645 preprocessed,
646 preprocessed_window,
647 public_values,
648 is_first_row: sels.is_first_row,
649 is_last_row: sels.is_last_row,
650 is_transition: sels.is_transition,
651 alpha: *alpha,
652 accumulator: SC::Challenge::ZERO,
653 };
654 let mut folder = VerifierConstraintFolderWithLookups {
655 inner: inner_folder,
656 permutation: VerticalPair::new(
657 RowMajorMatrixView::new_row(permutation_local),
658 RowMajorMatrixView::new_row(permutation_next),
659 ),
660 permutation_challenges,
661 permutation_values,
662 };
663 air.eval_with_lookups(&mut folder, lookups, lookup_gadget);
665 let folded_constraints = folder.inner.accumulator;
666
667 if folded_constraints * sels.inv_vanishing != *quotient {
669 return Err(VerificationError::OodEvaluationMismatch { index: None });
670 }
671
672 Ok(())
673}