1use alloc::string::String;
4use alloc::vec::Vec;
5use alloc::{format, vec};
6
7use itertools::Itertools;
8use p3_air::symbolic::SymbolicAirBuilder;
9use p3_air::{Air, RowWindow};
10use p3_challenger::{CanObserve, FieldChallenger};
11use p3_commit::{Pcs, PolynomialSpace};
12use p3_field::{BasedVectorSpace, Field, PrimeCharacteristicRing};
13use p3_matrix::dense::RowMajorMatrixView;
14use p3_matrix::stack::VerticalPair;
15use p3_util::zip_eq::zip_eq;
16use thiserror::Error;
17use tracing::instrument;
18
19use crate::symbolic::get_log_num_quotient_chunks;
20use crate::{
21 AirLayout, Domain, PcsError, PreprocessedVerifierKey, Proof, StarkGenericConfig, Val,
22 VerifierConstraintFolder,
23};
24
25pub fn recompose_quotient_from_chunks<SC>(
30 quotient_chunks_domains: &[Domain<SC>],
31 quotient_chunks: &[Vec<SC::Challenge>],
32 zeta: SC::Challenge,
33) -> SC::Challenge
34where
35 SC: StarkGenericConfig,
36{
37 let zps = quotient_chunks_domains
38 .iter()
39 .enumerate()
40 .map(|(i, domain)| {
41 quotient_chunks_domains
42 .iter()
43 .enumerate()
44 .filter(|(j, _)| *j != i)
45 .map(|(_, other_domain)| {
46 other_domain.vanishing_poly_at_point(zeta)
47 * other_domain
48 .vanishing_poly_at_point(domain.first_point())
49 .inverse()
50 })
51 .product::<SC::Challenge>()
52 })
53 .collect_vec();
54
55 quotient_chunks
56 .iter()
57 .enumerate()
58 .map(|(ch_i, ch)| {
59 zps[ch_i]
63 * ch.iter()
64 .enumerate()
65 .map(|(e_i, &c)| SC::Challenge::ith_basis_element(e_i).unwrap() * c)
66 .sum::<SC::Challenge>()
67 })
68 .sum::<SC::Challenge>()
69}
70
71#[allow(clippy::too_many_arguments)]
76pub fn verify_constraints<SC, A, PcsErr>(
77 air: &A,
78 trace_local: &[SC::Challenge],
79 trace_next: &[SC::Challenge],
80 preprocessed_local: Option<&[SC::Challenge]>,
81 preprocessed_next: Option<&[SC::Challenge]>,
82 public_values: &[Val<SC>],
83 trace_domain: Domain<SC>,
84 zeta: SC::Challenge,
85 alpha: SC::Challenge,
86 quotient: SC::Challenge,
87) -> Result<(), VerificationError<PcsErr>>
88where
89 SC: StarkGenericConfig,
90 A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
91 PcsErr: core::fmt::Debug,
92{
93 let sels = trace_domain.selectors_at_point(zeta);
94
95 let main = VerticalPair::new(
96 RowMajorMatrixView::new_row(trace_local),
97 RowMajorMatrixView::new_row(trace_next),
98 );
99
100 let preprocessed = match (preprocessed_local, preprocessed_next) {
101 (Some(local), Some(next)) => VerticalPair::new(
102 RowMajorMatrixView::new_row(local),
103 RowMajorMatrixView::new_row(next),
104 ),
105 _ => VerticalPair::new(
106 RowMajorMatrixView::new(&[], 0),
107 RowMajorMatrixView::new(&[], 0),
108 ),
109 };
110
111 let preprocessed_window =
112 RowWindow::from_two_rows(preprocessed.top.values, preprocessed.bottom.values);
113 let mut folder = VerifierConstraintFolder {
114 main,
115 preprocessed,
116 preprocessed_window,
117 public_values,
118 is_first_row: sels.is_first_row,
119 is_last_row: sels.is_last_row,
120 is_transition: sels.is_transition,
121 alpha,
122 accumulator: SC::Challenge::ZERO,
123 };
124 air.eval(&mut folder);
125 let folded_constraints = folder.accumulator;
126
127 if folded_constraints * sels.inv_vanishing != quotient {
129 return Err(VerificationError::OodEvaluationMismatch { index: None });
130 }
131
132 Ok(())
133}
134
135#[allow(clippy::type_complexity)]
138fn process_preprocessed_trace<SC, A>(
139 air: &A,
140 opened_values: &crate::proof::OpenedValues<SC::Challenge>,
141 preprocessed_vk: Option<&PreprocessedVerifierKey<SC>>,
142) -> Result<
143 (
144 usize,
145 Option<<SC::Pcs as Pcs<SC::Challenge, SC::Challenger>>::Commitment>,
146 ),
147 VerificationError<PcsError<SC>>,
148>
149where
150 SC: StarkGenericConfig,
151 A: for<'a> Air<VerifierConstraintFolder<'a, SC>>,
152{
153 let preprocessed_width = preprocessed_vk
157 .map(|vk| vk.width)
158 .or_else(|| air.preprocessed_trace().as_ref().map(|m| m.width))
159 .unwrap_or(0);
160
161 let preprocessed_local_len = opened_values
163 .preprocessed_local
164 .as_ref()
165 .map_or(0, |v| v.len());
166 let preprocessed_next_len = opened_values
167 .preprocessed_next
168 .as_ref()
169 .map_or(0, |v| v.len());
170 let expected_next_len = if !air.preprocessed_next_row_columns().is_empty() {
171 preprocessed_width
172 } else {
173 0
174 };
175 if preprocessed_width != preprocessed_local_len || expected_next_len != preprocessed_next_len {
176 return Err(VerificationError::InvalidProofShape);
178 }
179
180 match (preprocessed_width, preprocessed_vk) {
182 (0, None) => Ok((0, None)),
186
187 (w, Some(vk)) if w == vk.width => Ok((w, Some(vk.commitment.clone()))),
191
192 _ => Err(VerificationError::InvalidProofShape),
197 }
198}
199
200#[instrument(skip_all)]
201pub fn verify<SC, A>(
202 config: &SC,
203 air: &A,
204 proof: &Proof<SC>,
205 public_values: &[Val<SC>],
206) -> Result<(), VerificationError<PcsError<SC>>>
207where
208 SC: StarkGenericConfig,
209 A: Air<SymbolicAirBuilder<Val<SC>>> + for<'a> Air<VerifierConstraintFolder<'a, SC>>,
210{
211 verify_with_preprocessed(config, air, proof, public_values, None)
212}
213
214#[instrument(skip_all)]
215pub fn verify_with_preprocessed<SC, A>(
216 config: &SC,
217 air: &A,
218 proof: &Proof<SC>,
219 public_values: &[Val<SC>],
220 preprocessed_vk: Option<&PreprocessedVerifierKey<SC>>,
221) -> Result<(), VerificationError<PcsError<SC>>>
222where
223 SC: StarkGenericConfig,
224 A: Air<SymbolicAirBuilder<Val<SC>>> + for<'a> Air<VerifierConstraintFolder<'a, SC>>,
225{
226 let Proof {
227 commitments,
228 opened_values,
229 opening_proof,
230 degree_bits,
231 } = proof;
232
233 let pcs = config.pcs();
234 let degree = 1 << degree_bits;
235 let trace_domain = pcs.natural_domain_for_degree(degree);
236 let (preprocessed_width, preprocessed_commit) =
238 process_preprocessed_trace::<SC, A>(air, opened_values, preprocessed_vk)?;
239
240 if let Some(vk) = preprocessed_vk
242 && preprocessed_width > 0
243 && vk.degree_bits != *degree_bits
244 {
245 return Err(VerificationError::InvalidProofShape);
246 }
247
248 let layout = AirLayout {
249 preprocessed_width,
250 main_width: air.width(),
251 num_public_values: air.num_public_values(),
252 ..Default::default()
253 };
254 let log_num_quotient_chunks =
255 get_log_num_quotient_chunks::<Val<SC>, A>(air, layout, config.is_zk());
256 let num_quotient_chunks = 1 << (log_num_quotient_chunks + config.is_zk());
257 let mut challenger = config.initialise_challenger();
258 let init_trace_domain = pcs.natural_domain_for_degree(degree >> (config.is_zk()));
259
260 let quotient_domain =
261 trace_domain.create_disjoint_domain(1 << (degree_bits + log_num_quotient_chunks));
262 let quotient_chunks_domains = quotient_domain.split_domains(num_quotient_chunks);
263
264 let randomized_quotient_chunks_domains = quotient_chunks_domains
265 .iter()
266 .map(|domain| pcs.natural_domain_for_degree(domain.size() << (config.is_zk())))
267 .collect_vec();
268 if (opened_values.random.is_some() != SC::Pcs::ZK)
272 || (commitments.random.is_some() != SC::Pcs::ZK)
273 {
274 return Err(VerificationError::RandomizationError);
275 }
276
277 let air_width = A::width(air);
278 let main_next = !air.main_next_row_columns().is_empty();
279 let pre_next = !air.preprocessed_next_row_columns().is_empty();
280 let trace_next_ok = if main_next {
281 opened_values
282 .trace_next
283 .as_ref()
284 .is_some_and(|v| v.len() == air_width)
285 } else {
286 opened_values.trace_next.is_none()
287 };
288 let valid_shape = opened_values.trace_local.len() == air_width
289 && trace_next_ok
290 && opened_values.quotient_chunks.len() == num_quotient_chunks
291 && opened_values
292 .quotient_chunks
293 .iter()
294 .all(|qc| qc.len() == SC::Challenge::DIMENSION)
295 && opened_values.random.as_ref().is_none_or(|r_comm| r_comm.len() == SC::Challenge::DIMENSION);
297 if !valid_shape {
298 return Err(VerificationError::InvalidProofShape);
299 }
300
301 challenger.observe(Val::<SC>::from_usize(proof.degree_bits));
303 challenger.observe(Val::<SC>::from_usize(proof.degree_bits - config.is_zk()));
304 challenger.observe(Val::<SC>::from_usize(preprocessed_width));
305 challenger.observe(commitments.trace.clone());
311 if preprocessed_width > 0 {
312 challenger.observe(preprocessed_commit.as_ref().unwrap().clone());
313 }
314 challenger.observe_slice(public_values);
315
316 let alpha = challenger.sample_algebra_element();
321 challenger.observe(commitments.quotient_chunks.clone());
322
323 if let Some(r_commit) = commitments.random.clone() {
326 challenger.observe(r_commit);
327 }
328
329 let zeta = challenger.sample_algebra_element();
333 let zeta_next = init_trace_domain
334 .next_point(zeta)
335 .ok_or(VerificationError::NextPointUnavailable)?;
336
337 let mut coms_to_verify = if let Some(random_commit) = &commitments.random {
339 let random_values = opened_values
340 .random
341 .as_ref()
342 .ok_or(VerificationError::RandomizationError)?;
343 vec![(
344 random_commit.clone(),
345 vec![(trace_domain, vec![(zeta, random_values.clone())])],
346 )]
347 } else {
348 vec![]
349 };
350 let trace_round = {
351 let mut trace_points = vec![(zeta, opened_values.trace_local.clone())];
352 if main_next {
353 trace_points.push((
354 zeta_next,
355 opened_values
356 .trace_next
357 .clone()
358 .expect("checked in shape validation"),
359 ));
360 }
361 (
362 commitments.trace.clone(),
363 vec![(trace_domain, trace_points)],
364 )
365 };
366 coms_to_verify.extend(vec![
367 trace_round,
368 (
369 commitments.quotient_chunks.clone(),
370 zip_eq(
372 randomized_quotient_chunks_domains.iter(),
373 &opened_values.quotient_chunks,
374 VerificationError::InvalidProofShape,
375 )?
376 .map(|(domain, values)| (*domain, vec![(zeta, values.clone())]))
377 .collect_vec(),
378 ),
379 ]);
380
381 if preprocessed_width > 0 {
383 let mut pre_points = vec![(zeta, opened_values.preprocessed_local.clone().unwrap())];
384 if pre_next {
385 pre_points.push((zeta_next, opened_values.preprocessed_next.clone().unwrap()));
386 }
387 coms_to_verify.push((
388 preprocessed_commit.unwrap(),
389 vec![(trace_domain, pre_points)],
390 ));
391 }
392
393 pcs.verify(coms_to_verify, opening_proof, &mut challenger)
394 .map_err(VerificationError::InvalidOpeningArgument)?;
395
396 let quotient = recompose_quotient_from_chunks::<SC>(
397 "ient_chunks_domains,
398 &opened_values.quotient_chunks,
399 zeta,
400 );
401
402 let zeros;
403 let trace_next_slice = match &opened_values.trace_next {
404 Some(v) => v.as_slice(),
405 None => {
406 zeros = vec![SC::Challenge::ZERO; air_width];
407 &zeros
408 }
409 };
410 let pre_next_zeros;
411 let preprocessed_next_for_verify = match &opened_values.preprocessed_next {
412 Some(v) => Some(v.as_slice()),
413 None if preprocessed_width > 0 => {
414 pre_next_zeros = vec![SC::Challenge::ZERO; preprocessed_width];
415 Some(pre_next_zeros.as_slice())
416 }
417 None => None,
418 };
419 verify_constraints::<SC, A, PcsError<SC>>(
420 air,
421 &opened_values.trace_local,
422 trace_next_slice,
423 opened_values.preprocessed_local.as_deref(),
424 preprocessed_next_for_verify,
425 public_values,
426 init_trace_domain,
427 zeta,
428 alpha,
429 quotient,
430 )?;
431
432 Ok(())
433}
434
435#[derive(Debug, Error)]
436pub enum VerificationError<PcsErr>
437where
438 PcsErr: core::fmt::Debug,
439{
440 #[error("invalid proof shape")]
441 InvalidProofShape,
442 #[error("invalid opening argument: {0:?}")]
444 InvalidOpeningArgument(PcsErr),
445 #[error("out-of-domain evaluation mismatch{}", .index.map(|i| format!(" at index {}", i)).unwrap_or_default())]
448 OodEvaluationMismatch { index: Option<usize> },
449 #[error("randomization error: FRI batch randomization does not match ZK setting")]
451 RandomizationError,
452 #[error(
454 "next point unavailable: domain does not support computing the next point algebraically"
455 )]
456 NextPointUnavailable,
457 #[error("lookup error: {0}")]
459 LookupError(String),
460}