1use alloc::collections::BTreeMap;
2use alloc::vec;
3use alloc::vec::Vec;
4use core::marker::PhantomData;
5
6use itertools::{Itertools, izip};
7use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger};
8use p3_commit::{Mmcs, OpenedValues, Pcs, PeriodicLdeTable, PolynomialSpace};
9use p3_field::extension::ComplexExtendable;
10use p3_field::{ExtensionField, Field, batch_multiplicative_inverse, dot_product};
11use p3_fri::verifier::FriError;
12use p3_fri::{BatchMultiOpening, FriParameters};
13use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixCow};
14use p3_matrix::row_index_mapped::RowIndexMappedView;
15use p3_matrix::{Dimensions, Matrix};
16use p3_util::log2_strict_usize;
17use p3_util::zip_eq::zip_eq;
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20use tracing::{debug_span, info_span};
21
22use crate::deep_quotient::{
23 VanishingParts, accumulate_deep_quotient, compute_vanishing_parts, deep_quotient_reduce_row,
24 extract_lambda,
25};
26use crate::domain::CircleDomain;
27use crate::folding::{
28 CircleFriFolding, CircleFriFoldingForMmcs, fold_row_with_inv_twiddle, fold_y,
29};
30use crate::point::{Point, compute_lagrange_den_batched};
31use crate::prover::prove;
32use crate::verifier::verify;
33use crate::{
34 CfftPerm, CfftPermutable, CircleEvaluations, CircleFriProof, build_periodic_lde_table_circle,
35 cfft_permute_index, cfft_permute_slice,
36};
37
38#[derive(Clone, Debug)]
39pub struct CirclePcs<Val: Field, InputMmcs, FriMmcs> {
40 pub mmcs: InputMmcs,
41 pub fri_params: FriParameters<FriMmcs>,
42 pub _phantom: PhantomData<Val>,
43}
44
45impl<Val: Field, InputMmcs, FriMmcs> CirclePcs<Val, InputMmcs, FriMmcs> {
46 pub const fn new(mmcs: InputMmcs, fri_params: FriParameters<FriMmcs>) -> Self {
47 Self {
48 mmcs,
49 fri_params,
50 _phantom: PhantomData,
51 }
52 }
53}
54
55#[derive(Serialize, Deserialize, Clone)]
56#[serde(bound = "")]
57pub struct CircleInputProof<
58 Val: Field,
59 Challenge: Field,
60 InputMmcs: Mmcs<Val>,
61 FriMmcs: Mmcs<Challenge>,
62> {
63 input_openings: Vec<BatchMultiOpening<Val, InputMmcs>>,
66 first_layer_siblings: Vec<Vec<Challenge>>,
68 first_layer_proof: FriMmcs::MultiProof,
70}
71
72#[derive(Debug, Error)]
73pub enum InputError<InputMmcsError, FriMmcsError>
74where
75 InputMmcsError: core::fmt::Debug,
76 FriMmcsError: core::fmt::Debug,
77{
78 #[error("input MMCS error: {0:?}")]
79 InputMmcsError(InputMmcsError),
80 #[error("first layer MMCS error: {0:?}")]
81 FirstLayerMmcsError(FriMmcsError),
82 #[error("input shape error: mismatched dimensions")]
83 InputShapeError,
84 #[error("opening point coincides with a query point")]
88 OpeningPointMatchesQueryPoint,
89 #[error(
90 "batch {batch}, matrix {matrix}: opened at no points; its width cannot be authenticated"
91 )]
92 MatrixWithoutOpeningPoints { batch: usize, matrix: usize },
93}
94
95#[derive(Serialize, Deserialize, Clone)]
96#[serde(bound(
97 serialize = "Witness: Serialize",
98 deserialize = "Witness: Deserialize<'de>"
99))]
100pub struct CirclePcsProof<
101 Val: Field,
102 Challenge: Field,
103 InputMmcs: Mmcs<Val>,
104 FriMmcs: Mmcs<Challenge>,
105 Witness,
106> {
107 first_layer_commitment: FriMmcs::Commitment,
108 lambdas: Vec<Challenge>,
109 fri_proof: CircleFriProof<
110 Challenge,
111 FriMmcs,
112 Witness,
113 CircleInputProof<Val, Challenge, InputMmcs, FriMmcs>,
114 >,
115}
116
117impl<Val, InputMmcs, FriMmcs, Challenge, Challenger> Pcs<Challenge, Challenger>
118 for CirclePcs<Val, InputMmcs, FriMmcs>
119where
120 Val: ComplexExtendable,
121 Challenge: ExtensionField<Val>,
122 InputMmcs: Mmcs<Val>,
123 FriMmcs: Mmcs<Challenge>,
124 Challenger: FieldChallenger<Val> + GrindingChallenger + CanObserve<FriMmcs::Commitment>,
125{
126 type Domain = CircleDomain<Val>;
127 type Commitment = InputMmcs::Commitment;
128 type ProverData = InputMmcs::ProverData<RowMajorMatrix<Val>>;
129 type EvaluationsOnDomain<'a> = RowIndexMappedView<CfftPerm, RowMajorMatrixCow<'a, Val>>;
130 type Proof = CirclePcsProof<Val, Challenge, InputMmcs, FriMmcs, Challenger::Witness>;
131 type Error = FriError<FriMmcs::Error, InputError<InputMmcs::Error, FriMmcs::Error>>;
132 const ZK: bool = false;
133
134 fn natural_domain_for_degree(&self, degree: usize) -> Self::Domain {
135 CircleDomain::standard(log2_strict_usize(degree))
136 }
137
138 fn log_max_lde_height(&self) -> usize {
139 Val::CIRCLE_TWO_ADICITY - 1
140 }
141
142 fn commit(
143 &self,
144 evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
145 ) -> (Self::Commitment, Self::ProverData) {
146 let ldes = evaluations
147 .into_iter()
148 .map(|(domain, evals)| {
149 assert!(
150 domain.log_n >= 2,
151 "CirclePcs cannot commit to a matrix with fewer than 4 rows.",
152 );
154 CircleEvaluations::from_natural_order(domain, evals)
155 .extrapolate(CircleDomain::standard(
156 domain.log_n + self.fri_params.log_blowup,
157 ))
158 .to_cfft_order()
159 })
160 .collect_vec();
161 let (comm, mmcs_data) = self.mmcs.commit(ldes);
162 (comm, mmcs_data)
163 }
164
165 fn get_quotient_ldes(
166 &self,
167 evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
168 _num_chunks: usize,
169 ) -> Vec<RowMajorMatrix<Val>> {
170 evaluations
171 .into_iter()
172 .map(|(domain, evals)| {
173 assert!(
174 domain.log_n >= 2,
175 "CirclePcs cannot commit to a matrix with fewer than 4 rows.",
176 );
178 CircleEvaluations::from_natural_order(domain, evals)
179 .extrapolate(CircleDomain::standard(
180 domain.log_n + self.fri_params.log_blowup,
181 ))
182 .to_cfft_order()
183 })
184 .collect_vec()
185 }
186
187 fn commit_ldes(&self, ldes: Vec<RowMajorMatrix<Val>>) -> (Self::Commitment, Self::ProverData) {
188 self.mmcs.commit(ldes)
189 }
190
191 fn get_evaluations_on_domain<'a>(
192 &self,
193 data: &'a Self::ProverData,
194 idx: usize,
195 domain: Self::Domain,
196 ) -> Self::EvaluationsOnDomain<'a> {
197 let mat = self.mmcs.get_matrices(data)[idx].as_view();
198 let committed_domain = CircleDomain::standard(log2_strict_usize(mat.height()));
199 if domain == committed_domain {
200 mat.as_cow().cfft_perm_rows()
201 } else {
202 let log_sub = committed_domain.log_n - self.fri_params.log_blowup;
211 let sub_domain = CircleDomain::new(log_sub, committed_domain.shift);
212 let coeffs =
213 CircleEvaluations::from_cfft_order(sub_domain, mat.split_rows(1 << log_sub).0)
214 .interpolate();
215 CircleEvaluations::evaluate(domain, coeffs)
216 .to_cfft_order()
217 .as_cow()
218 .cfft_perm_rows()
219 }
220 }
221
222 fn open(
223 &self,
224 rounds: Vec<(
226 &Self::ProverData,
227 Vec<
229 Vec<Challenge>,
231 >,
232 )>,
233 challenger: &mut Challenger,
234 ) -> (OpenedValues<Challenge>, Self::Proof) {
235 let mut permuted_points: BTreeMap<usize, Vec<Point<Val>>> = BTreeMap::new();
239 debug_span!("materialize domain points").in_scope(|| {
240 for (data, _) in &rounds {
241 for mat in self.mmcs.get_matrices(data) {
242 let log_height = log2_strict_usize(mat.height());
243 permuted_points.entry(log_height).or_insert_with(|| {
244 cfft_permute_slice(&CircleDomain::standard(log_height).points_vec())
245 });
246 }
247 }
248 });
249
250 let mut lagrange_dens: Vec<((usize, Challenge), Vec<Challenge>)> = vec![];
252
253 let values: OpenedValues<Challenge> = rounds
255 .iter()
256 .map(|(data, points_for_mats)| {
257 let mats = self.mmcs.get_matrices(data);
258 debug_assert_eq!(
259 mats.len(),
260 points_for_mats.len(),
261 "Mismatched number of matrices and points"
262 );
263 izip!(mats, points_for_mats)
264 .map(|(mat, points_for_mat)| {
265 let log_height = log2_strict_usize(mat.height());
266 let log_sub = log_height - self.fri_params.log_blowup;
273 let sub_height = 1 << log_sub;
274 let sub_domain = CircleDomain::new(
275 log_sub,
276 CircleDomain::<Val>::standard(log_height).shift,
277 );
278 let evals = CircleEvaluations::from_cfft_order(
280 sub_domain,
281 mat.split_rows(sub_height).0,
282 );
283
284 let den_idxs = points_for_mat
286 .iter()
287 .map(|&zeta_uni| {
288 let key = (log_height, zeta_uni);
289 lagrange_dens
290 .iter()
291 .position(|(k, _)| *k == key)
292 .unwrap_or_else(|| {
293 let den = info_span!("compute Lagrange denominators")
294 .in_scope(|| {
295 compute_lagrange_den_batched(
296 &permuted_points[&log_height][..sub_height],
297 Point::from_projective_line(zeta_uni),
298 log_sub,
299 )
300 });
301 lagrange_dens.push((key, den));
302 lagrange_dens.len() - 1
303 })
304 })
305 .collect_vec();
306
307 let ps_for_points: Vec<Vec<Challenge>> =
308 debug_span!("compute opened values with Lagrange interpolation")
309 .in_scope(|| match (&points_for_mat[..], &den_idxs[..]) {
310 (&[zeta_0, zeta_1], &[idx_0, idx_1]) => evals
313 .evaluate_at_two_points_with_dens(
314 [
315 Point::from_projective_line(zeta_0),
316 Point::from_projective_line(zeta_1),
317 ],
318 [&lagrange_dens[idx_0].1, &lagrange_dens[idx_1].1],
319 )
320 .into(),
321 _ => izip!(points_for_mat, &den_idxs)
322 .map(|(&zeta_uni, &den_idx)| {
323 evals.evaluate_at_point_with_den(
324 Point::from_projective_line(zeta_uni),
325 &lagrange_dens[den_idx].1,
326 )
327 })
328 .collect(),
329 });
330
331 for ps_at_zeta in &ps_for_points {
332 challenger.observe_algebra_slice(ps_at_zeta);
333 }
334 ps_for_points
335 })
336 .collect()
337 })
338 .collect();
339 drop(lagrange_dens);
340
341 let alpha: Challenge = challenger.sample_algebra_element();
343
344 let mut reduced_openings: BTreeMap<usize, (Challenge, Vec<Challenge>)> = BTreeMap::new();
356
357 let mut vanishing_parts: Vec<((usize, Challenge), VanishingParts<Challenge>)> = vec![];
359
360 rounds
361 .iter()
362 .zip(values.iter())
363 .for_each(|((data, points_for_mats), values)| {
364 let mats = self.mmcs.get_matrices(data);
365 izip!(mats, points_for_mats, values).for_each(|(mat, points_for_mat, values)| {
366 let log_height = log2_strict_usize(mat.height());
367 let log_sub = log_height - self.fri_params.log_blowup;
368
369 let (alpha_offset, reduced_opening_for_log_height) = reduced_openings
370 .entry(log_height)
371 .or_insert_with(|| (Challenge::ONE, Challenge::zero_vec(1 << log_height)));
372
373 const LIFT_MIN_WIDTH: usize = 1024;
378
379 let reduced_rows = if log_sub > 0 && mat.width() >= LIFT_MIN_WIDTH {
385 let sub_domain = CircleDomain::new(
386 log_sub,
387 CircleDomain::<Val>::standard(log_height).shift,
388 );
389 CircleEvaluations::from_cfft_order(
390 sub_domain,
391 mat.split_rows(1 << log_sub).0,
392 )
393 .rowwise_alpha_reduce_lifted(alpha, CircleDomain::standard(log_height))
394 } else {
395 CircleEvaluations::from_cfft_order(
396 CircleDomain::standard(log_height),
397 mat.as_view(),
398 )
399 .rowwise_alpha_reduce(alpha)
400 };
401 let alpha_pow_width = alpha.exp_u64(mat.width() as u64);
402
403 points_for_mat
404 .iter()
405 .zip(values.iter())
406 .for_each(|(&zeta_uni, ps_at_zeta)| {
407 let zeta = Point::from_projective_line(zeta_uni);
408 let key = (log_height, zeta_uni);
409 let vp_idx = vanishing_parts
410 .iter()
411 .position(|(k, _)| *k == key)
412 .unwrap_or_else(|| {
413 let vp = compute_vanishing_parts(
414 &permuted_points[&log_height],
415 zeta,
416 );
417 vanishing_parts.push((key, vp));
418 vanishing_parts.len() - 1
419 });
420
421 let reduced_ps_at_zeta: Challenge =
423 dot_product(alpha.powers(), ps_at_zeta.iter().copied());
424
425 accumulate_deep_quotient(
428 reduced_opening_for_log_height,
429 *alpha_offset,
430 alpha_pow_width,
431 &reduced_rows,
432 &vanishing_parts[vp_idx].1,
433 reduced_ps_at_zeta,
434 );
435
436 *alpha_offset *= alpha_pow_width.square();
438 });
439 });
440 });
441 drop(vanishing_parts);
442
443 let mut lambdas = vec![];
447 let mut log_heights = vec![];
448 let first_layer_mats: Vec<RowMajorMatrix<Challenge>> = reduced_openings
449 .into_iter()
450 .map(|(log_height, (_, mut ro))| {
451 assert!(log_height > 0);
452 log_heights.push(log_height);
453 let lambda = extract_lambda(&mut ro, self.fri_params.log_blowup);
454 lambdas.push(lambda);
455 RowMajorMatrix::new(ro, 2)
457 })
458 .collect();
459 let log_max_height = log_heights.iter().max().copied().unwrap();
460
461 let (first_layer_commitment, first_layer_data) =
467 self.fri_params.mmcs.commit(first_layer_mats);
468 challenger.observe(first_layer_commitment.clone());
469 let bivariate_beta: Challenge = challenger.sample_algebra_element();
470
471 let fri_input: Vec<Vec<Challenge>> = self
474 .fri_params
475 .mmcs
476 .get_matrices(&first_layer_data)
477 .into_iter()
478 .map(|m| fold_y(bivariate_beta, m))
479 .rev()
481 .collect();
482
483 let folding: CircleFriFoldingForMmcs<Val, Challenge, InputMmcs, FriMmcs> =
484 CircleFriFolding(PhantomData);
485
486 let fri_proof = prove(
487 &folding,
488 &self.fri_params,
489 fri_input,
490 challenger,
491 |indices| {
492 let input_openings = rounds
498 .iter()
499 .map(|(data, _)| {
500 let log_max_batch_height =
501 log2_strict_usize(self.mmcs.get_max_height(data));
502 let bits_reduced = log_max_height - log_max_batch_height;
503 let reduced_indices: Vec<usize> =
504 indices.iter().map(|&index| index >> bits_reduced).collect();
505 let (opened_values, opening_proof) =
506 self.mmcs.open_multi_batch(&reduced_indices, data);
507 BatchMultiOpening {
508 opened_values,
509 opening_proof,
510 }
511 })
512 .collect();
513
514 let paired_indices: Vec<usize> = indices.iter().map(|&index| index >> 1).collect();
517 let (first_layer_values, first_layer_proof) = self
518 .fri_params
519 .mmcs
520 .open_multi_batch(&paired_indices, &first_layer_data);
521 let first_layer_siblings = izip!(indices, first_layer_values)
522 .map(|(&index, values)| {
523 izip!(&values, &log_heights)
524 .map(|(v, log_height)| {
525 let reduced_index = index >> (log_max_height - log_height);
526 let sibling_index = (reduced_index & 1) ^ 1;
527 v[sibling_index]
528 })
529 .collect()
530 })
531 .collect();
532 CircleInputProof {
533 input_openings,
534 first_layer_siblings,
535 first_layer_proof,
536 }
537 },
538 );
539
540 (
541 values,
542 CirclePcsProof {
543 first_layer_commitment,
544 lambdas,
545 fri_proof,
546 },
547 )
548 }
549
550 fn verify(
551 &self,
552 rounds: Vec<(
554 Self::Commitment,
555 Vec<(
557 Self::Domain,
559 Vec<(
561 Challenge,
563 Vec<Challenge>,
565 )>,
566 )>,
567 )>,
568 proof: &Self::Proof,
569 challenger: &mut Challenger,
570 ) -> Result<(), Self::Error> {
571 for (_, round) in &rounds {
573 for (_, mat) in round {
574 for (_, point) in mat {
575 challenger.observe_algebra_slice(point);
576 }
577 }
578 }
579
580 let alpha: Challenge = challenger.sample_algebra_element();
582
583 let matrix_alpha_pows: Vec<Vec<(Challenge, Challenge)>> = rounds
589 .iter()
590 .map(|(_, mats)| {
591 mats.iter()
592 .map(|(_, points_and_values)| {
593 let width = points_and_values.first().map_or(0, |(_, v)| v.len());
594 let alpha_pow_width = alpha.exp_u64(width as u64);
595 (alpha_pow_width, alpha_pow_width.square())
596 })
597 .collect()
598 })
599 .collect();
600 let max_width = rounds
601 .iter()
602 .flat_map(|(_, mats)| mats.iter())
603 .flat_map(|(_, points_and_values)| points_and_values.iter().map(|(_, v)| v.len()))
604 .max()
605 .unwrap_or(0);
606 let alpha_powers: Vec<Challenge> = alpha.powers().collect_n(max_width);
607
608 challenger.observe(proof.first_layer_commitment.clone());
609 let bivariate_beta: Challenge = challenger.sample_algebra_element();
610
611 let log_global_max_height =
613 proof.fri_proof.commit_phase_commits.len() + self.fri_params.log_blowup + 1;
614
615 let expected_log_global_max_height = rounds
628 .iter()
629 .flat_map(|(_, mats)| {
630 mats.iter()
631 .map(|(domain, _)| domain.log_n + self.fri_params.log_blowup)
632 })
633 .max();
634 if let Some(expected) = expected_log_global_max_height
635 && log_global_max_height < expected
636 {
637 return Err(FriError::GlobalMaxHeightMismatch {
638 expected,
639 got: log_global_max_height,
640 });
641 }
642
643 let folding: CircleFriFoldingForMmcs<Val, Challenge, InputMmcs, FriMmcs> =
644 CircleFriFolding(PhantomData);
645
646 verify(
647 &folding,
648 &self.fri_params,
649 &proof.fri_proof,
650 challenger,
651 |indices, input_proof| {
652 let CircleInputProof {
653 input_openings,
654 first_layer_siblings,
655 first_layer_proof,
656 } = input_proof;
657
658 if first_layer_siblings.len() != indices.len() {
660 return Err(InputError::InputShapeError);
661 }
662 for batch_opening in input_openings {
663 if batch_opening.opened_values.len() != indices.len() {
664 return Err(InputError::InputShapeError);
665 }
666 }
667
668 for (batch, (batch_opening, (batch_commit, mats))) in
671 zip_eq(input_openings, &rounds, InputError::InputShapeError)?.enumerate()
672 {
673 let batch_heights: Vec<usize> = mats
674 .iter()
675 .map(|(domain, _)| domain.size() << self.fri_params.log_blowup)
676 .collect_vec();
677 for opened_values in &batch_opening.opened_values {
679 if opened_values.len() != mats.len() {
680 return Err(InputError::InputShapeError);
681 }
682 }
683 let batch_dims: Vec<Dimensions> = batch_heights
684 .iter()
685 .zip(mats)
686 .enumerate()
687 .map(|(matrix, (&height, (_, points_and_values)))| {
688 let (_, values) = points_and_values
698 .first()
699 .ok_or(InputError::MatrixWithoutOpeningPoints { batch, matrix })?;
700 Ok(Dimensions {
701 width: values.len(),
702 height,
703 })
704 })
705 .collect::<Result<Vec<_>, _>>()?;
706
707 let (dims, reduced_indices) = batch_heights
708 .iter()
709 .max()
710 .map(|x| log2_strict_usize(*x))
711 .map_or_else(
712 ||
713 (&[][..], vec![0; indices.len()]),
715 |log_batch_max_height| {
716 let bits_reduced = log_global_max_height - log_batch_max_height;
717 (
718 &batch_dims[..],
719 indices.iter().map(|&i| i >> bits_reduced).collect_vec(),
720 )
721 },
722 );
723
724 self.mmcs
725 .verify_multi_batch(
726 batch_commit,
727 dims,
728 &reduced_indices,
729 &batch_opening.opened_values,
730 &batch_opening.opening_proof,
731 )
732 .map_err(InputError::InputMmcsError)?;
733 }
734
735 let mut all_fri_inputs = Vec::with_capacity(indices.len());
738 let mut fl_leaves_by_query = Vec::with_capacity(indices.len());
739 let mut fl_dims: Vec<Dimensions> = Vec::new();
740
741 for (query, &index) in indices.iter().enumerate() {
742 let mut reduced_openings = BTreeMap::new();
744
745 for (batch, (batch_opening, (_, mats))) in
746 zip_eq(input_openings, &rounds, InputError::InputShapeError)?.enumerate()
747 {
748 for (matrix, (ps_at_x, (mat_domain, mat_points_and_values))) in zip_eq(
749 &batch_opening.opened_values[query],
750 mats,
751 InputError::InputShapeError,
752 )?
753 .enumerate()
754 {
755 let log_height = mat_domain.log_n + self.fri_params.log_blowup;
756 let bits_reduced = log_global_max_height - log_height;
757 let orig_idx = cfft_permute_index(index >> bits_reduced, log_height);
758
759 let committed_domain = CircleDomain::standard(log_height);
760 let x = committed_domain.nth_point(orig_idx);
761
762 let (alpha_offset, ro) = reduced_openings
763 .entry(log_height)
764 .or_insert((Challenge::ONE, Challenge::ZERO));
765 let (alpha_pow_width, alpha_pow_width_2) =
766 matrix_alpha_pows[batch][matrix];
767
768 for (zeta_uni, ps_at_zeta) in mat_points_and_values {
769 if ps_at_zeta.len() != ps_at_x.len() {
772 return Err(InputError::InputShapeError);
773 }
774 let zeta = Point::from_projective_line(*zeta_uni);
775
776 *ro += *alpha_offset
779 * deep_quotient_reduce_row(
780 alpha_pow_width,
781 &alpha_powers,
782 x,
783 zeta,
784 ps_at_x,
785 ps_at_zeta,
786 )
787 .ok_or(InputError::OpeningPointMatchesQueryPoint)?;
788
789 *alpha_offset *= alpha_pow_width_2;
790 }
791 }
792 }
793
794 let per_height: Vec<_> = zip_eq(
804 zip_eq(
805 reduced_openings,
806 &first_layer_siblings[query],
807 InputError::InputShapeError,
808 )?,
809 &proof.lambdas,
810 InputError::InputShapeError,
811 )?
812 .map(|(((log_height, (_, ro)), &fl_sib), &lambda)| {
813 assert!(log_height > 0);
814
815 let orig_size = log_height - self.fri_params.log_blowup;
816 let bits_reduced = log_global_max_height - log_height;
817 let b = (index >> bits_reduced) & 1;
818 let orig_idx = cfft_permute_index(index >> bits_reduced, log_height);
819
820 let lde_domain = CircleDomain::standard(log_height);
821 let p: Point<Val> = lde_domain.nth_point(orig_idx);
822
823 let lambda_corrected = ro - lambda * p.v_n(orig_size);
824
825 let mut fl_values = vec![lambda_corrected; 2];
826 fl_values[b ^ 1] = fl_sib;
827
828 let y_twiddle = if b == 0 { p.y } else { -p.y };
829
830 let dims = Dimensions {
831 width: 2,
833 height: 1 << (log_height - 1),
834 };
835
836 (log_height, y_twiddle, fl_values, dims)
837 })
838 .collect();
839
840 let y_twiddles_inv = batch_multiplicative_inverse(
841 &per_height.iter().map(|&(_, t, _, _)| t).collect_vec(),
842 );
843
844 let (mut fri_input, query_fl_dims, fl_leaves): (Vec<_>, Vec<_>, Vec<_>) =
845 per_height
846 .into_iter()
847 .zip(y_twiddles_inv)
848 .map(|((log_height, _, fl_values, dims), y_twiddle_inv)| {
849 let fri_input = (
850 log_height - 1,
852 fold_row_with_inv_twiddle(
853 y_twiddle_inv,
854 bivariate_beta,
855 fl_values.iter().copied(),
856 ),
857 );
858 (fri_input, dims, fl_values)
859 })
860 .multiunzip();
861
862 fri_input.reverse();
864
865 if query == 0 {
867 fl_dims = query_fl_dims;
868 }
869
870 all_fri_inputs.push(fri_input);
871 fl_leaves_by_query.push(fl_leaves);
872 }
873
874 let paired_indices = indices.iter().map(|&i| i >> 1).collect_vec();
876 self.fri_params
877 .mmcs
878 .verify_multi_batch(
879 &proof.first_layer_commitment,
880 &fl_dims,
881 &paired_indices,
882 &fl_leaves_by_query,
883 first_layer_proof,
884 )
885 .map_err(InputError::FirstLayerMmcsError)?;
886
887 Ok(all_fri_inputs)
888 },
889 )
890 }
891
892 fn build_periodic_lde_table(
893 &self,
894 periodic_cols: &[Vec<Val>],
895 trace_domain: Self::Domain,
896 quotient_domain: Self::Domain,
897 ) -> PeriodicLdeTable<Val> {
898 build_periodic_lde_table_circle(periodic_cols, &trace_domain, "ient_domain)
899 }
900}
901
902#[cfg(test)]
903mod tests {
904 use p3_challenger::{HashChallenger, SerializingChallenger32};
905 use p3_commit::ExtensionMmcs;
906 use p3_field::PrimeCharacteristicRing;
907 use p3_field::extension::BinomialExtensionField;
908 use p3_fri::FriParameters;
909 use p3_fri::verifier::FriError;
910 use p3_keccak::Keccak256Hash;
911 use p3_merkle_tree::MerkleTreeMmcs;
912 use p3_mersenne_31::Mersenne31;
913 use p3_symmetric::{CompressionFunctionFromHasher, SerializingHasher};
914 use rand::rngs::SmallRng;
915 use rand::{RngExt, SeedableRng};
916
917 use super::*;
918
919 type Val = Mersenne31;
920 type Challenge = BinomialExtensionField<Mersenne31, 3>;
921 type ByteHash = Keccak256Hash;
922 type FieldHash = SerializingHasher<ByteHash>;
923 type MyCompress = CompressionFunctionFromHasher<ByteHash, 2, 32>;
924 type ValMmcs = MerkleTreeMmcs<Val, u8, FieldHash, MyCompress, 2, 32>;
925 type ChallengeMmcs = ExtensionMmcs<Val, Challenge, ValMmcs>;
926 type Challenger = SerializingChallenger32<Val, HashChallenger<u8, ByteHash, 32>>;
927 type TestPcs = CirclePcs<Val, ValMmcs, ChallengeMmcs>;
928 type TestError = FriError<
929 <ChallengeMmcs as Mmcs<Challenge>>::Error,
930 InputError<<ValMmcs as Mmcs<Val>>::Error, <ChallengeMmcs as Mmcs<Challenge>>::Error>,
931 >;
932
933 #[allow(clippy::type_complexity)]
945 fn setup_valid_proof() -> (
946 TestPcs,
947 ByteHash,
948 <ValMmcs as Mmcs<Val>>::Commitment,
949 CircleDomain<Val>,
950 Challenge,
951 Vec<Vec<Vec<Vec<Challenge>>>>,
952 CirclePcsProof<Val, Challenge, ValMmcs, ChallengeMmcs, Val>,
953 ) {
954 let mut rng = SmallRng::seed_from_u64(0);
955
956 let byte_hash = ByteHash {};
958 let field_hash = FieldHash::new(byte_hash);
959 let compress = MyCompress::new(byte_hash);
960 let val_mmcs = ValMmcs::new(field_hash, compress, 0);
961
962 let challenge_mmcs = ChallengeMmcs::new(val_mmcs.clone());
964
965 let fri_params = FriParameters::new_testing(challenge_mmcs, 0);
967
968 let pcs = TestPcs {
969 mmcs: val_mmcs,
970 fri_params,
971 _phantom: PhantomData,
972 };
973
974 let log_n = 10;
976 let d =
977 <TestPcs as Pcs<Challenge, Challenger>>::natural_domain_for_degree(&pcs, 1 << log_n);
978
979 let evals = RowMajorMatrix::rand(&mut rng, 1 << log_n, 1);
980
981 let (comm, data) = <TestPcs as Pcs<Challenge, Challenger>>::commit(&pcs, [(d, evals)]);
983
984 let zeta: Challenge = rng.random();
986
987 let mut chal = Challenger::from_hasher(vec![], byte_hash);
989 let (values, proof) = pcs.open(vec![(&data, vec![vec![zeta]])], &mut chal);
990
991 (pcs, byte_hash, comm, d, zeta, values, proof)
992 }
993
994 fn try_verify(
1000 pcs: &TestPcs,
1001 byte_hash: ByteHash,
1002 comm: &<ValMmcs as Mmcs<Val>>::Commitment,
1003 d: CircleDomain<Val>,
1004 zeta: Challenge,
1005 values: &[Vec<Vec<Vec<Challenge>>>],
1006 proof: &CirclePcsProof<Val, Challenge, ValMmcs, ChallengeMmcs, Val>,
1007 ) -> Result<(), TestError> {
1008 let mut chal = Challenger::from_hasher(vec![], byte_hash);
1011 pcs.verify(
1012 vec![(
1013 comm.clone(),
1014 vec![(d, vec![(zeta, values[0][0][0].clone())])],
1015 )],
1016 proof,
1017 &mut chal,
1018 )
1019 }
1020
1021 #[test]
1022 fn circle_pcs() {
1023 let (pcs, byte_hash, comm, d, zeta, values, proof) = setup_valid_proof();
1025 try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof).expect("verify err");
1026 }
1027
1028 #[test]
1029 fn reject_matrix_without_opening_points() {
1030 let mut rng = SmallRng::seed_from_u64(0);
1045
1046 let byte_hash = ByteHash {};
1047 let field_hash = FieldHash::new(byte_hash);
1048 let compress = MyCompress::new(byte_hash);
1049 let val_mmcs = ValMmcs::new(field_hash, compress, 0);
1050 let challenge_mmcs = ChallengeMmcs::new(val_mmcs.clone());
1051 let fri_params = FriParameters::new_testing(challenge_mmcs, 0);
1052 let pcs = TestPcs {
1053 mmcs: val_mmcs,
1054 fri_params,
1055 _phantom: PhantomData,
1056 };
1057
1058 let log_n = 10;
1060 let d =
1061 <TestPcs as Pcs<Challenge, Challenger>>::natural_domain_for_degree(&pcs, 1 << log_n);
1062 let evals_0 = RowMajorMatrix::rand(&mut rng, 1 << log_n, 1);
1063 let evals_1 = RowMajorMatrix::rand(&mut rng, 1 << log_n, 1);
1064 let (comm, data) =
1065 <TestPcs as Pcs<Challenge, Challenger>>::commit(&pcs, [(d, evals_0), (d, evals_1)]);
1066
1067 let zeta: Challenge = rng.random();
1069 let mut chal = Challenger::from_hasher(vec![], byte_hash);
1070 let (values, proof) = pcs.open(vec![(&data, vec![vec![zeta], vec![]])], &mut chal);
1071
1072 let mut chal = Challenger::from_hasher(vec![], byte_hash);
1074 let err = pcs
1075 .verify(
1076 vec![(
1077 comm,
1078 vec![(d, vec![(zeta, values[0][0][0].clone())]), (d, vec![])],
1079 )],
1080 &proof,
1081 &mut chal,
1082 )
1083 .expect_err("matrix without opening points must be rejected");
1084
1085 let FriError::InputError(InputError::MatrixWithoutOpeningPoints { batch, matrix }) = err
1087 else {
1088 panic!("expected MatrixWithoutOpeningPoints, got {err:?}");
1089 };
1090 assert_eq!(batch, 0);
1091 assert_eq!(matrix, 1);
1092 }
1093
1094 #[test]
1095 fn get_evaluations_on_domain_matches_direct_lde() {
1096 let mut rng = SmallRng::seed_from_u64(1);
1101
1102 let byte_hash = ByteHash {};
1103 let field_hash = FieldHash::new(byte_hash);
1104 let compress = MyCompress::new(byte_hash);
1105 let val_mmcs = ValMmcs::new(field_hash, compress, 0);
1106 let challenge_mmcs = ChallengeMmcs::new(val_mmcs.clone());
1107
1108 let mut fri_params = FriParameters::new_testing(challenge_mmcs, 0);
1110 fri_params.log_blowup = 2;
1111
1112 let pcs = TestPcs {
1113 mmcs: val_mmcs,
1114 fri_params,
1115 _phantom: PhantomData,
1116 };
1117
1118 let log_n = 8;
1119 let width = 3;
1120 let d =
1121 <TestPcs as Pcs<Challenge, Challenger>>::natural_domain_for_degree(&pcs, 1 << log_n);
1122 let evals = RowMajorMatrix::<Val>::rand(&mut rng, 1 << log_n, width);
1123
1124 let (_comm, data) =
1125 <TestPcs as Pcs<Challenge, Challenger>>::commit(&pcs, [(d, evals.clone())]);
1126
1127 for target_log_n in [log_n, log_n + 1, log_n + 2, log_n + 3] {
1131 let target = CircleDomain::standard(target_log_n);
1132 let got = <TestPcs as Pcs<Challenge, Challenger>>::get_evaluations_on_domain(
1133 &pcs, &data, 0, target,
1134 )
1135 .to_row_major_matrix();
1136
1137 let expected = CircleEvaluations::from_natural_order(d, evals.clone())
1139 .extrapolate(target)
1140 .to_natural_order()
1141 .to_row_major_matrix();
1142
1143 assert_eq!(got, expected, "mismatch for target_log_n = {target_log_n}");
1144 }
1145 }
1146
1147 #[test]
1148 fn reject_commit_phase_query_count_mismatch() {
1149 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1152
1153 proof.fri_proof.commit_phase_openings[0]
1159 .sibling_values
1160 .pop();
1161
1162 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1163 .expect_err("expected CommitPhaseQueryCountMismatch");
1164
1165 let FriError::CommitPhaseQueryCountMismatch {
1167 round,
1168 expected,
1169 got,
1170 } = err
1171 else {
1172 panic!("expected CommitPhaseQueryCountMismatch, got {err:?}");
1173 };
1174 assert_eq!(round, 0);
1175 assert_eq!(expected, pcs.fri_params.num_queries);
1176 assert_eq!(got, pcs.fri_params.num_queries - 1);
1177 }
1178
1179 #[test]
1180 fn reject_zero_queries() {
1181 let (mut pcs, byte_hash, comm, d, zeta, values, proof) = setup_valid_proof();
1189 pcs.fri_params.num_queries = 0;
1190
1191 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1192 .expect_err("zero-query instance must be rejected");
1193
1194 assert!(
1195 matches!(err, FriError::ZeroQueries),
1196 "expected ZeroQueries, got {err:?}"
1197 );
1198 }
1199
1200 #[test]
1201 #[should_panic(expected = "num_queries must be at least 1")]
1202 fn prover_rejects_zero_queries() {
1203 let mut rng = SmallRng::seed_from_u64(0);
1206
1207 let byte_hash = ByteHash {};
1209 let field_hash = FieldHash::new(byte_hash);
1210 let compress = MyCompress::new(byte_hash);
1211 let val_mmcs = ValMmcs::new(field_hash, compress, 0);
1212 let challenge_mmcs = ChallengeMmcs::new(val_mmcs.clone());
1213
1214 let mut fri_params = FriParameters::new_testing(challenge_mmcs, 0);
1216 fri_params.num_queries = 0;
1217
1218 let pcs = TestPcs {
1219 mmcs: val_mmcs,
1220 fri_params,
1221 _phantom: PhantomData,
1222 };
1223
1224 let log_n = 10;
1226 let d =
1227 <TestPcs as Pcs<Challenge, Challenger>>::natural_domain_for_degree(&pcs, 1 << log_n);
1228 let evals = RowMajorMatrix::rand(&mut rng, 1 << log_n, 1);
1229 let (_comm, data) = <TestPcs as Pcs<Challenge, Challenger>>::commit(&pcs, [(d, evals)]);
1230
1231 let zeta: Challenge = rng.random();
1233 let mut chal = Challenger::from_hasher(vec![], byte_hash);
1234 let _ = pcs.open(vec![(&data, vec![vec![zeta]])], &mut chal);
1235 }
1236
1237 #[test]
1238 fn reject_commit_pow_witness_count_mismatch() {
1239 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1240 let num_rounds = proof.fri_proof.commit_phase_commits.len();
1241
1242 proof.fri_proof.commit_pow_witnesses.pop();
1244
1245 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1246 .expect_err("expected CommitPowWitnessCountMismatch");
1247
1248 let FriError::CommitPowWitnessCountMismatch { expected, got } = err else {
1249 panic!("expected CommitPowWitnessCountMismatch, got {err:?}");
1250 };
1251 assert_eq!(expected, num_rounds);
1252 assert_eq!(got, num_rounds - 1);
1253 }
1254
1255 #[test]
1256 fn reject_under_reported_commit_rounds() {
1257 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1263
1264 let log_blowup = pcs.fri_params.log_blowup;
1269 let expected = d.log_n + log_blowup;
1270 let original = proof.fri_proof.commit_phase_commits.len() + log_blowup + 1;
1271 assert_eq!(original, expected, "fixture must start height-consistent");
1272
1273 proof.fri_proof.commit_phase_commits.pop();
1279
1280 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1281 .expect_err("expected GlobalMaxHeightMismatch");
1282
1283 let FriError::GlobalMaxHeightMismatch { expected: exp, got } = err else {
1284 panic!("expected GlobalMaxHeightMismatch, got {err:?}");
1285 };
1286 assert_eq!(exp, expected);
1288 assert_eq!(got, expected - 1);
1290 }
1291
1292 #[test]
1293 fn reject_commit_phase_openings_count_mismatch() {
1294 let (pcs, byte_hash, comm, d, zeta, values, proof) = setup_valid_proof();
1298
1299 let mut bad = proof.clone();
1302
1303 bad.fri_proof.commit_phase_openings.pop();
1309
1310 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &bad)
1311 .expect_err("expected CommitPhaseOpeningsCountMismatch");
1312
1313 let FriError::CommitPhaseOpeningsCountMismatch { expected, got } = err else {
1314 panic!("expected CommitPhaseOpeningsCountMismatch, got {err:?}");
1315 };
1316 assert_eq!(expected, proof.fri_proof.commit_phase_commits.len());
1317 assert_eq!(got, expected - 1);
1318 }
1319
1320 #[test]
1321 fn reject_sibling_values_length_mismatch() {
1322 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1326
1327 let log_arity = proof.fri_proof.commit_phase_openings[0].log_arity as usize;
1329 let arity = 1usize << log_arity;
1330 let original_sibling_count =
1331 proof.fri_proof.commit_phase_openings[0].sibling_values[0].len();
1332
1333 proof.fri_proof.commit_phase_openings[0].sibling_values[0].pop();
1340
1341 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1342 .expect_err("expected SiblingValuesLengthMismatch");
1343
1344 let FriError::SiblingValuesLengthMismatch {
1345 round,
1346 expected,
1347 got,
1348 } = err
1349 else {
1350 panic!("expected SiblingValuesLengthMismatch, got {err:?}");
1351 };
1352 assert_eq!(round, 0);
1354 assert_eq!(expected, arity - 1);
1356 assert_eq!(got, original_sibling_count - 1);
1358 }
1359
1360 #[test]
1376 fn reject_input_openings_query_count_mismatch() {
1377 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1385
1386 proof.fri_proof.input_openings.first_layer_siblings.pop();
1392
1393 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1394 .expect_err("expected InputShapeError");
1395
1396 assert!(
1397 matches!(err, FriError::InputError(InputError::InputShapeError)),
1398 "expected InputShapeError, got {err:?}"
1399 );
1400 }
1401
1402 #[test]
1403 fn reject_tampered_commit_phase_sibling_value() {
1404 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1413
1414 proof.fri_proof.commit_phase_openings[0].sibling_values[0][0] += Challenge::ONE;
1415
1416 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1417 .expect_err("a tampered sibling value must be rejected");
1418
1419 assert!(
1420 matches!(err, FriError::FinalPolyMismatch),
1421 "expected FinalPolyMismatch, got {err:?}"
1422 );
1423 }
1424
1425 #[test]
1426 fn reject_tampered_commit_phase_opening_proof() {
1427 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1431
1432 proof.fri_proof.commit_phase_openings[0]
1433 .opening_proof
1434 .sibling_hashes[0] = Default::default();
1435
1436 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1437 .expect_err("a tampered commit-phase digest must be rejected");
1438
1439 assert!(
1440 matches!(err, FriError::CommitPhaseMmcsError(_)),
1441 "expected CommitPhaseMmcsError, got {err:?}"
1442 );
1443 }
1444
1445 #[test]
1446 fn reject_tampered_first_layer_proof() {
1447 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1451
1452 proof
1453 .fri_proof
1454 .input_openings
1455 .first_layer_proof
1456 .sibling_hashes[0] = Default::default();
1457
1458 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1459 .expect_err("a tampered first-layer digest must be rejected");
1460
1461 assert!(
1462 matches!(
1463 err,
1464 FriError::InputError(InputError::FirstLayerMmcsError(_))
1465 ),
1466 "expected FirstLayerMmcsError, got {err:?}"
1467 );
1468 }
1469
1470 #[test]
1471 fn reject_invalid_log_arity() {
1472 let (pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1474
1475 proof.fri_proof.commit_phase_openings[0].log_arity = 0;
1477
1478 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1479 .expect_err("expected InvalidLogArity");
1480
1481 let FriError::InvalidLogArity {
1482 round,
1483 log_arity,
1484 max,
1485 } = err
1486 else {
1487 panic!("expected InvalidLogArity, got {err:?}");
1488 };
1489 assert_eq!(round, 0);
1490 assert_eq!(log_arity, 0);
1491 assert_eq!(max, pcs.fri_params.max_log_arity);
1492 }
1493
1494 #[test]
1495 fn reject_global_max_height_too_large() {
1496 let (mut pcs, byte_hash, comm, d, zeta, values, mut proof) = setup_valid_proof();
1501
1502 pcs.fri_params.commit_proof_of_work_bits = 0;
1505 pcs.fri_params.query_proof_of_work_bits = 0;
1506
1507 let extra_query_index_bits = 1;
1512 let commit = proof.fri_proof.commit_phase_commits[0].clone();
1513 let witness = proof.fri_proof.commit_pow_witnesses[0];
1514 while proof.fri_proof.commit_phase_commits.len()
1515 + pcs.fri_params.log_blowup
1516 + extra_query_index_bits
1517 < Val::CIRCLE_TWO_ADICITY
1518 {
1519 proof.fri_proof.commit_phase_commits.push(commit.clone());
1520 proof.fri_proof.commit_pow_witnesses.push(witness);
1521 let opening = proof.fri_proof.commit_phase_openings[0].clone();
1523 proof.fri_proof.commit_phase_openings.push(opening);
1524 }
1525
1526 let err = try_verify(&pcs, byte_hash, &comm, d, zeta, &values, &proof)
1527 .expect_err("expected GlobalMaxHeightTooLarge");
1528
1529 let FriError::GlobalMaxHeightTooLarge {
1530 log_global_max_height,
1531 two_adicity,
1532 } = err
1533 else {
1534 panic!("expected GlobalMaxHeightTooLarge, got {err:?}");
1535 };
1536 assert_eq!(two_adicity, Val::CIRCLE_TWO_ADICITY);
1538 assert!(log_global_max_height >= two_adicity);
1540 }
1541}