1use core::fmt::Display;
2use std::{
3 fmt::{Debug, Formatter},
4 marker::PhantomData,
5};
6
7use itertools::Itertools;
8use p3_air::{Air, BaseAir};
9use p3_challenger::{CanObserve, FieldChallenger};
10use p3_commit::{LagrangeSelectors, Pcs, PolynomialSpace};
11use p3_field::{AbstractExtensionField, AbstractField, Field, PrimeField64, TwoAdicField};
12
13use super::{
14 folder::VerifierConstraintFolder,
15 types::{AirOpenedValues, ChipOpenedValues, ShardCommitment, ShardProof},
16 Domain, OpeningError, StarkGenericConfig, StarkVerifyingKey, Val,
17};
18use crate::{
19 air::{InteractionScope, MachineAir},
20 InteractionKind, MachineChip,
21};
22
23pub struct Verifier<SC, A>(PhantomData<SC>, PhantomData<A>);
25
26impl<SC: StarkGenericConfig, A: MachineAir<Val<SC>>> Verifier<SC, A> {
27 #[allow(clippy::too_many_lines)]
29 pub fn verify_shard(
30 config: &SC,
31 vk: &StarkVerifyingKey<SC>,
32 chips: &[&MachineChip<SC, A>],
33 challenger: &mut SC::Challenger,
34 proof: &ShardProof<SC>,
35 ) -> Result<(), VerificationError<SC>>
36 where
37 A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
38 {
39 use itertools::izip;
40
41 let ShardProof {
42 commitment,
43 opened_values,
44 opening_proof,
45 chip_ordering,
46 public_values,
47 ..
48 } = proof;
49
50 let pcs = config.pcs();
51
52 if chips.len() != opened_values.chips.len() {
53 return Err(VerificationError::ChipOpeningLengthMismatch);
54 }
55
56 let log_degrees = opened_values.chips.iter().map(|val| val.log_degree).collect::<Vec<_>>();
57
58 let log_quotient_degrees =
59 chips.iter().map(|chip| chip.log_quotient_degree()).collect::<Vec<_>>();
60
61 for ((chip, log_degree), log_quotient_degree) in
63 chips.iter().zip_eq(log_degrees.iter()).zip_eq(log_quotient_degrees.iter())
64 {
65 if log_degree.saturating_add(*log_quotient_degree) > SC::Val::TWO_ADICITY {
66 return Err(VerificationError::InvalidLogDegree(chip.name(), *log_degree));
67 }
68 }
69
70 for kind in InteractionKind::all_kinds() {
75 let mut max_lookup_mult = 0u64;
76 chips.iter().zip(opened_values.chips.iter()).for_each(|(chip, val)| {
77 max_lookup_mult = max_lookup_mult.saturating_add(
78 (chip.num_sends_by_kind(kind) as u64 + chip.num_receives_by_kind(kind) as u64)
79 .saturating_mul(1u64 << val.log_degree),
80 );
81 });
82 if max_lookup_mult >= SC::Val::ORDER_U64 {
83 return Err(VerificationError::LookupMultiplicityOverflow);
84 }
85 }
86
87 let trace_domains = log_degrees
88 .iter()
89 .map(|log_degree| pcs.natural_domain_for_degree(1 << log_degree))
90 .collect::<Vec<_>>();
91
92 let ShardCommitment { main_commit, permutation_commit, quotient_commit } = commitment;
93
94 challenger.observe(main_commit.clone());
95
96 let local_permutation_challenges =
97 (0..2).map(|_| challenger.sample_ext_element::<SC::Challenge>()).collect::<Vec<_>>();
98
99 challenger.observe(permutation_commit.clone());
100 for (opening, chip) in opened_values.chips.iter().zip_eq(chips.iter()) {
103 let local_sum = opening.local_cumulative_sum;
104 let global_sum = opening.global_cumulative_sum;
105
106 challenger.observe_slice(local_sum.as_base_slice());
107 challenger.observe_slice(&global_sum.0.x.0);
108 challenger.observe_slice(&global_sum.0.y.0);
109
110 if chip.commit_scope() == InteractionScope::Local && !global_sum.is_zero() {
111 return Err(VerificationError::CumulativeSumsError(
112 "global cumulative sum is non-zero, but chip is Local",
113 ));
114 }
115
116 let has_local_interactions = chip
117 .sends()
118 .iter()
119 .chain(chip.receives())
120 .any(|i| i.scope == InteractionScope::Local);
121 if !has_local_interactions && !local_sum.is_zero() {
122 return Err(VerificationError::CumulativeSumsError(
123 "local cumulative sum is non-zero, but no local interactions",
124 ));
125 }
126 }
127
128 let alpha = challenger.sample_ext_element::<SC::Challenge>();
129
130 challenger.observe(quotient_commit.clone());
132
133 let zeta = challenger.sample_ext_element::<SC::Challenge>();
134
135 let preprocessed_domains_points_and_opens = vk
136 .chip_information
137 .iter()
138 .map(|(name, domain, _)| {
139 let i = *chip_ordering.get(name).filter(|&&i| i < chips.len()).ok_or(
140 VerificationError::PreprocessedChipIdMismatch(name.clone(), String::new()),
141 )?;
142 if name != &chips[i].name() {
143 return Err(VerificationError::PreprocessedChipIdMismatch(
144 name.clone(),
145 chips[i].name(),
146 ));
147 }
148 let values = opened_values.chips[i].preprocessed.clone();
149 if !chips[i].local_only() {
150 Ok((
151 *domain,
152 vec![(zeta, values.local), (domain.next_point(zeta).unwrap(), values.next)],
153 ))
154 } else {
155 Ok((*domain, vec![(zeta, values.local)]))
156 }
157 })
158 .collect::<Result<Vec<_>, _>>()?;
159
160 let main_domains_points_and_opens = trace_domains
161 .iter()
162 .zip_eq(opened_values.chips.iter())
163 .zip_eq(chips.iter())
164 .map(|((domain, values), chip)| {
165 if !chip.local_only() {
166 (
167 *domain,
168 vec![
169 (zeta, values.main.local.clone()),
170 (domain.next_point(zeta).unwrap(), values.main.next.clone()),
171 ],
172 )
173 } else {
174 (*domain, vec![(zeta, values.main.local.clone())])
175 }
176 })
177 .collect::<Vec<_>>();
178
179 let perm_domains_points_and_opens = trace_domains
180 .iter()
181 .zip_eq(opened_values.chips.iter())
182 .map(|(domain, values)| {
183 (
184 *domain,
185 vec![
186 (zeta, values.permutation.local.clone()),
187 (domain.next_point(zeta).unwrap(), values.permutation.next.clone()),
188 ],
189 )
190 })
191 .collect::<Vec<_>>();
192
193 let quotient_chunk_domains = trace_domains
194 .iter()
195 .zip_eq(log_degrees)
196 .zip_eq(log_quotient_degrees)
197 .map(|((domain, log_degree), log_quotient_degree)| {
198 let quotient_degree = 1 << log_quotient_degree;
199 let quotient_domain =
200 domain.create_disjoint_domain(1 << (log_degree + log_quotient_degree));
201 quotient_domain.split_domains(quotient_degree)
202 })
203 .collect::<Vec<_>>();
204
205 let quotient_domains_points_and_opens = proof
206 .opened_values
207 .chips
208 .iter()
209 .zip_eq(quotient_chunk_domains.iter())
210 .flat_map(|(values, qc_domains)| {
211 values
212 .quotient
213 .iter()
214 .zip_eq(qc_domains)
215 .map(move |(values, q_domain)| (*q_domain, vec![(zeta, values.clone())]))
216 })
217 .collect::<Vec<_>>();
218
219 let rounds = vec![
220 (vk.commit.clone(), preprocessed_domains_points_and_opens),
221 (main_commit.clone(), main_domains_points_and_opens),
222 (permutation_commit.clone(), perm_domains_points_and_opens),
223 (quotient_commit.clone(), quotient_domains_points_and_opens),
224 ];
225
226 config
227 .pcs()
228 .verify(rounds, opening_proof, challenger)
229 .map_err(|e| VerificationError::InvalidopeningArgument(e))?;
230
231 let permutation_challenges = local_permutation_challenges;
232
233 for (chip, trace_domain, qc_domains, values) in
235 izip!(chips.iter(), trace_domains, quotient_chunk_domains, opened_values.chips.iter(),)
236 {
237 Self::verify_opening_shape(chip, values)
239 .map_err(|e| VerificationError::OpeningShapeError(chip.name(), e))?;
240 Self::verify_constraints(
242 chip,
243 values,
244 trace_domain,
245 qc_domains,
246 zeta,
247 alpha,
248 &permutation_challenges,
249 public_values,
250 )
251 .map_err(|_| VerificationError::OodEvaluationMismatch(chip.name()))?;
252 }
253 let local_cumulative_sum = proof.local_cumulative_sum();
255 if local_cumulative_sum != SC::Challenge::zero() {
256 return Err(VerificationError::CumulativeSumsError("local cumulative sum is not zero"));
257 }
258
259 Ok(())
260 }
261
262 fn verify_opening_shape(
263 chip: &MachineChip<SC, A>,
264 opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
265 ) -> Result<(), OpeningShapeError> {
266 if opening.preprocessed.local.len() != chip.preprocessed_width() {
268 return Err(OpeningShapeError::PreprocessedWidthMismatch(
269 chip.preprocessed_width(),
270 opening.preprocessed.local.len(),
271 ));
272 }
273 if opening.preprocessed.next.len() != chip.preprocessed_width() {
274 return Err(OpeningShapeError::PreprocessedWidthMismatch(
275 chip.preprocessed_width(),
276 opening.preprocessed.next.len(),
277 ));
278 }
279
280 if opening.main.local.len() != chip.width() {
282 return Err(OpeningShapeError::MainWidthMismatch(
283 chip.width(),
284 opening.main.local.len(),
285 ));
286 }
287 if opening.main.next.len() != chip.width() {
288 return Err(OpeningShapeError::MainWidthMismatch(chip.width(), opening.main.next.len()));
289 }
290
291 if opening.permutation.local.len() != chip.permutation_width() * SC::Challenge::D {
293 return Err(OpeningShapeError::PermutationWidthMismatch(
294 chip.permutation_width(),
295 opening.permutation.local.len(),
296 ));
297 }
298 if opening.permutation.next.len() != chip.permutation_width() * SC::Challenge::D {
299 return Err(OpeningShapeError::PermutationWidthMismatch(
300 chip.permutation_width(),
301 opening.permutation.next.len(),
302 ));
303 }
304 if opening.quotient.len() != chip.quotient_width() {
306 return Err(OpeningShapeError::QuotientWidthMismatch(
307 chip.quotient_width(),
308 opening.quotient.len(),
309 ));
310 }
311 for slice in &opening.quotient {
314 if slice.len() != SC::Challenge::D {
315 return Err(OpeningShapeError::QuotientChunkSizeMismatch(
316 SC::Challenge::D,
317 slice.len(),
318 ));
319 }
320 }
321
322 Ok(())
323 }
324
325 #[allow(clippy::too_many_arguments)]
326 #[allow(clippy::needless_pass_by_value)]
327 fn verify_constraints(
328 chip: &MachineChip<SC, A>,
329 opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
330 trace_domain: Domain<SC>,
331 qc_domains: Vec<Domain<SC>>,
332 zeta: SC::Challenge,
333 alpha: SC::Challenge,
334 permutation_challenges: &[SC::Challenge],
335 public_values: &[Val<SC>],
336 ) -> Result<(), OodEvaluationMismatch>
337 where
338 A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
339 {
340 let sels = trace_domain.selectors_at_point(zeta);
341
342 let quotient = Self::recompute_quotient(opening, &qc_domains, zeta);
344 let folded_constraints = Self::eval_constraints(
346 chip,
347 opening,
348 &sels,
349 alpha,
350 permutation_challenges,
351 public_values,
352 );
353
354 if folded_constraints * sels.inv_zeroifier == quotient {
357 Ok(())
358 } else {
359 Err(OodEvaluationMismatch)
360 }
361 }
362
363 pub fn eval_constraints(
365 chip: &MachineChip<SC, A>,
366 opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
367 selectors: &LagrangeSelectors<SC::Challenge>,
368 alpha: SC::Challenge,
369 permutation_challenges: &[SC::Challenge],
370 public_values: &[Val<SC>],
371 ) -> SC::Challenge
372 where
373 A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
374 {
375 let unflatten = |v: &[SC::Challenge]| {
378 v.chunks_exact(SC::Challenge::D)
379 .map(|chunk| {
380 chunk.iter().enumerate().map(|(e_i, &x)| SC::Challenge::monomial(e_i) * x).sum()
381 })
382 .collect::<Vec<SC::Challenge>>()
383 };
384
385 let perm_opening = AirOpenedValues {
386 local: unflatten(&opening.permutation.local),
387 next: unflatten(&opening.permutation.next),
388 };
389
390 let mut folder = VerifierConstraintFolder::<SC> {
391 preprocessed: opening.preprocessed.view(),
392 main: opening.main.view(),
393 perm: perm_opening.view(),
394 perm_challenges: permutation_challenges,
395 local_cumulative_sum: &opening.local_cumulative_sum,
396 global_cumulative_sum: &opening.global_cumulative_sum,
397 is_first_row: selectors.is_first_row,
398 is_last_row: selectors.is_last_row,
399 is_transition: selectors.is_transition,
400 alpha,
401 accumulator: SC::Challenge::zero(),
402 public_values,
403 _marker: PhantomData,
404 };
405
406 chip.eval(&mut folder);
407
408 folder.accumulator
409 }
410
411 pub fn recompute_quotient(
413 opening: &ChipOpenedValues<Val<SC>, SC::Challenge>,
414 qc_domains: &[Domain<SC>],
415 zeta: SC::Challenge,
416 ) -> SC::Challenge {
417 use p3_field::Field;
418
419 let zps = qc_domains
420 .iter()
421 .enumerate()
422 .map(|(i, domain)| {
423 qc_domains
424 .iter()
425 .enumerate()
426 .filter(|(j, _)| *j != i)
427 .map(|(_, other_domain)| {
428 other_domain.zp_at_point(zeta) *
429 other_domain.zp_at_point(domain.first_point()).inverse()
430 })
431 .product::<SC::Challenge>()
432 })
433 .collect_vec();
434
435 opening
436 .quotient
437 .iter()
438 .enumerate()
439 .map(|(ch_i, ch)| {
440 assert_eq!(ch.len(), SC::Challenge::D);
442 ch.iter()
443 .enumerate()
444 .map(|(e_i, &c)| zps[ch_i] * SC::Challenge::monomial(e_i) * c)
445 .sum::<SC::Challenge>()
446 })
447 .sum::<SC::Challenge>()
448 }
449}
450
451pub struct OodEvaluationMismatch;
453
454pub enum OpeningShapeError {
456 PreprocessedWidthMismatch(usize, usize),
458 MainWidthMismatch(usize, usize),
460 PermutationWidthMismatch(usize, usize),
462 QuotientWidthMismatch(usize, usize),
464 QuotientChunkSizeMismatch(usize, usize),
466}
467
468pub enum VerificationError<SC: StarkGenericConfig> {
470 InvalidopeningArgument(OpeningError<SC>),
472 OodEvaluationMismatch(String),
476 OpeningShapeError(String, OpeningShapeError),
478 MissingCpuChip,
480 ChipOpeningLengthMismatch,
482 PreprocessedChipIdMismatch(String, String),
484 CumulativeSumsError(&'static str),
486 InvalidLogDegree(String, usize),
488 LookupMultiplicityOverflow,
490}
491
492impl Debug for OpeningShapeError {
493 #[allow(clippy::uninlined_format_args)]
494 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
495 match self {
496 OpeningShapeError::PreprocessedWidthMismatch(expected, actual) => {
497 write!(f, "Preprocessed width mismatch: expected {}, got {}", expected, actual)
498 }
499 OpeningShapeError::MainWidthMismatch(expected, actual) => {
500 write!(f, "Main width mismatch: expected {}, got {}", expected, actual)
501 }
502 OpeningShapeError::PermutationWidthMismatch(expected, actual) => {
503 write!(f, "Permutation width mismatch: expected {}, got {}", expected, actual)
504 }
505 OpeningShapeError::QuotientWidthMismatch(expected, actual) => {
506 write!(f, "Quotient width mismatch: expected {}, got {}", expected, actual)
507 }
508 OpeningShapeError::QuotientChunkSizeMismatch(expected, actual) => {
509 write!(f, "Quotient chunk size mismatch: expected {}, got {}", expected, actual)
510 }
511 }
512 }
513}
514
515impl Display for OpeningShapeError {
516 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
517 write!(f, "{self:?}")
518 }
519}
520
521impl<SC: StarkGenericConfig> Debug for VerificationError<SC> {
522 #[allow(clippy::uninlined_format_args)]
523 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
524 match self {
525 VerificationError::InvalidopeningArgument(e) => {
526 write!(f, "Invalid opening argument: {:?}", e)
527 }
528 VerificationError::OodEvaluationMismatch(chip) => {
529 write!(f, "Out-of-domain evaluation mismatch on chip {}", chip)
530 }
531 VerificationError::OpeningShapeError(chip, e) => {
532 write!(f, "Invalid opening shape for chip {}: {:?}", chip, e)
533 }
534 VerificationError::MissingCpuChip => {
535 write!(f, "Missing CPU chip")
536 }
537 VerificationError::ChipOpeningLengthMismatch => {
538 write!(f, "Chip opening length mismatch")
539 }
540 VerificationError::PreprocessedChipIdMismatch(expected, actual) => {
541 write!(f, "Preprocessed chip id mismatch: expected {}, got {}", expected, actual)
542 }
543 VerificationError::CumulativeSumsError(s) => write!(f, "cumulative sums error: {}", s),
544 VerificationError::InvalidLogDegree(chip, log_degree) => {
545 write!(f, "Invalid log degree for chip {}: got {}", chip, log_degree)
546 }
547 VerificationError::LookupMultiplicityOverflow => {
548 write!(f, "Lookup multiplicity overflow")
549 }
550 }
551 }
552}
553
554impl<SC: StarkGenericConfig> Display for VerificationError<SC> {
555 #[allow(clippy::uninlined_format_args)]
556 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
557 match self {
558 VerificationError::InvalidopeningArgument(_) => {
559 write!(f, "Invalid opening argument")
560 }
561 VerificationError::OodEvaluationMismatch(chip) => {
562 write!(f, "Out-of-domain evaluation mismatch on chip {}", chip)
563 }
564 VerificationError::OpeningShapeError(chip, e) => {
565 write!(f, "Invalid opening shape for chip {}: {}", chip, e)
566 }
567 VerificationError::MissingCpuChip => {
568 write!(f, "Missing CPU chip in shard")
569 }
570 VerificationError::ChipOpeningLengthMismatch => {
571 write!(f, "Chip opening length mismatch")
572 }
573 VerificationError::CumulativeSumsError(s) => write!(f, "cumulative sums error: {}", s),
574 VerificationError::PreprocessedChipIdMismatch(expected, actual) => {
575 write!(f, "Preprocessed chip id mismatch: expected {}, got {}", expected, actual)
576 }
577 VerificationError::InvalidLogDegree(chip, log_degree) => {
578 write!(f, "Invalid log degree for chip {}: got {}", chip, log_degree)
579 }
580 VerificationError::LookupMultiplicityOverflow => {
581 write!(f, "Lookup multiplicity overflow")
582 }
583 }
584 }
585}
586
587impl<SC: StarkGenericConfig> std::error::Error for VerificationError<SC> {}