p3_circle/
pcs.rs

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::{BatchOpening, BatchOpeningRef, Mmcs, OpenedValues, Pcs, PolynomialSpace};
9use p3_field::extension::ComplexExtendable;
10use p3_field::{ExtensionField, Field};
11use p3_fri::FriParameters;
12use p3_fri::verifier::FriError;
13use p3_matrix::dense::{RowMajorMatrix, RowMajorMatrixCow};
14use p3_matrix::row_index_mapped::RowIndexMappedView;
15use p3_matrix::{Dimensions, Matrix};
16use p3_maybe_rayon::prelude::*;
17use p3_util::log2_strict_usize;
18use p3_util::zip_eq::zip_eq;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21use tracing::info_span;
22
23use crate::deep_quotient::{deep_quotient_reduce_row, extract_lambda};
24use crate::domain::CircleDomain;
25use crate::folding::{CircleFriFolding, CircleFriFoldingForMmcs, fold_y, fold_y_row};
26use crate::point::Point;
27use crate::prover::prove;
28use crate::verifier::verify;
29use crate::{CfftPerm, CfftPermutable, CircleEvaluations, CircleFriProof, cfft_permute_index};
30
31#[derive(Debug)]
32pub struct CirclePcs<Val: Field, InputMmcs, FriMmcs> {
33    pub mmcs: InputMmcs,
34    pub fri_params: FriParameters<FriMmcs>,
35    pub _phantom: PhantomData<Val>,
36}
37
38impl<Val: Field, InputMmcs, FriMmcs> CirclePcs<Val, InputMmcs, FriMmcs> {
39    pub const fn new(mmcs: InputMmcs, fri_params: FriParameters<FriMmcs>) -> Self {
40        Self {
41            mmcs,
42            fri_params,
43            _phantom: PhantomData,
44        }
45    }
46}
47
48#[derive(Serialize, Deserialize, Clone)]
49#[serde(bound = "")]
50pub struct CircleInputProof<
51    Val: Field,
52    Challenge: Field,
53    InputMmcs: Mmcs<Val>,
54    FriMmcs: Mmcs<Challenge>,
55> {
56    input_openings: Vec<BatchOpening<Val, InputMmcs>>,
57    first_layer_siblings: Vec<Challenge>,
58    first_layer_proof: FriMmcs::Proof,
59}
60
61#[derive(Debug, Error)]
62pub enum InputError<InputMmcsError, FriMmcsError>
63where
64    InputMmcsError: core::fmt::Debug,
65    FriMmcsError: core::fmt::Debug,
66{
67    #[error("input MMCS error: {0:?}")]
68    InputMmcsError(InputMmcsError),
69    #[error("first layer MMCS error: {0:?}")]
70    FirstLayerMmcsError(FriMmcsError),
71    #[error("input shape error: mismatched dimensions")]
72    InputShapeError,
73}
74
75#[derive(Serialize, Deserialize, Clone)]
76#[serde(bound(
77    serialize = "Witness: Serialize",
78    deserialize = "Witness: Deserialize<'de>"
79))]
80pub struct CirclePcsProof<
81    Val: Field,
82    Challenge: Field,
83    InputMmcs: Mmcs<Val>,
84    FriMmcs: Mmcs<Challenge>,
85    Witness,
86> {
87    first_layer_commitment: FriMmcs::Commitment,
88    lambdas: Vec<Challenge>,
89    fri_proof: CircleFriProof<
90        Challenge,
91        FriMmcs,
92        Witness,
93        CircleInputProof<Val, Challenge, InputMmcs, FriMmcs>,
94    >,
95}
96
97impl<Val, InputMmcs, FriMmcs, Challenge, Challenger> Pcs<Challenge, Challenger>
98    for CirclePcs<Val, InputMmcs, FriMmcs>
99where
100    Val: ComplexExtendable,
101    Challenge: ExtensionField<Val>,
102    InputMmcs: Mmcs<Val>,
103    FriMmcs: Mmcs<Challenge>,
104    Challenger: FieldChallenger<Val> + GrindingChallenger + CanObserve<FriMmcs::Commitment>,
105{
106    type Domain = CircleDomain<Val>;
107    type Commitment = InputMmcs::Commitment;
108    type ProverData = InputMmcs::ProverData<RowMajorMatrix<Val>>;
109    type EvaluationsOnDomain<'a> = RowIndexMappedView<CfftPerm, RowMajorMatrixCow<'a, Val>>;
110    type Proof = CirclePcsProof<Val, Challenge, InputMmcs, FriMmcs, Challenger::Witness>;
111    type Error = FriError<FriMmcs::Error, InputError<InputMmcs::Error, FriMmcs::Error>>;
112    const ZK: bool = false;
113
114    fn natural_domain_for_degree(&self, degree: usize) -> Self::Domain {
115        CircleDomain::standard(log2_strict_usize(degree))
116    }
117
118    fn commit(
119        &self,
120        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
121    ) -> (Self::Commitment, Self::ProverData) {
122        let ldes = evaluations
123            .into_iter()
124            .map(|(domain, evals)| {
125                assert!(
126                    domain.log_n >= 2,
127                    "CirclePcs cannot commit to a matrix with fewer than 4 rows.",
128                    // (because we bivariate fold one bit, and fri needs one more bit)
129                );
130                CircleEvaluations::from_natural_order(domain, evals)
131                    .extrapolate(CircleDomain::standard(
132                        domain.log_n + self.fri_params.log_blowup,
133                    ))
134                    .to_cfft_order()
135            })
136            .collect_vec();
137        let (comm, mmcs_data) = self.mmcs.commit(ldes);
138        (comm, mmcs_data)
139    }
140
141    fn get_evaluations_on_domain<'a>(
142        &self,
143        data: &'a Self::ProverData,
144        idx: usize,
145        domain: Self::Domain,
146    ) -> Self::EvaluationsOnDomain<'a> {
147        let mat = self.mmcs.get_matrices(data)[idx].as_view();
148        let committed_domain = CircleDomain::standard(log2_strict_usize(mat.height()));
149        if domain == committed_domain {
150            mat.as_cow().cfft_perm_rows()
151        } else {
152            CircleEvaluations::from_cfft_order(committed_domain, mat)
153                .extrapolate(domain)
154                .to_cfft_order()
155                .as_cow()
156                .cfft_perm_rows()
157        }
158    }
159
160    fn open(
161        &self,
162        // For each round,
163        rounds: Vec<(
164            &Self::ProverData,
165            // for each matrix,
166            Vec<
167                // points to open
168                Vec<Challenge>,
169            >,
170        )>,
171        challenger: &mut Challenger,
172    ) -> (OpenedValues<Challenge>, Self::Proof) {
173        // Open matrices at points
174        let values: OpenedValues<Challenge> = rounds
175            .iter()
176            .map(|(data, points_for_mats)| {
177                let mats = self.mmcs.get_matrices(data);
178                debug_assert_eq!(
179                    mats.len(),
180                    points_for_mats.len(),
181                    "Mismatched number of matrices and points"
182                );
183                izip!(mats, points_for_mats)
184                    .map(|(mat, points_for_mat)| {
185                        let log_height = log2_strict_usize(mat.height());
186                        // It was committed in cfft order.
187                        let evals = CircleEvaluations::from_cfft_order(
188                            CircleDomain::standard(log_height),
189                            mat.as_view(),
190                        );
191                        points_for_mat
192                            .iter()
193                            .map(|&zeta| {
194                                let zeta = Point::from_projective_line(zeta);
195                                let ps_at_zeta =
196                                    info_span!("compute opened values with Lagrange interpolation")
197                                        .in_scope(|| evals.evaluate_at_point(zeta));
198                                challenger.observe_algebra_slice(&ps_at_zeta);
199                                ps_at_zeta
200                            })
201                            .collect()
202                    })
203                    .collect()
204            })
205            .collect();
206
207        // Batch combination challenge
208        let alpha: Challenge = challenger.sample_algebra_element();
209
210        /*
211        We are reducing columns ("ro" = reduced opening) with powers of alpha:
212          ro = .. + α^n c_n + α^(n+1) c_(n+1) + ..
213        But we want to precompute small powers of alpha, and batch the columns. So we can do:
214          ro = .. + α^n (α^0 c_n + α^1 c_(n+1) + ..) + ..
215        reusing the α^0, α^1, etc., then at the end of each column batch we multiply by the α^n.
216        (Due to circle stark specifics, we need 2 powers of α for each column, so actually α^(2n)).
217        We store this α^(2n), the running reducing factor per log_height, and call it the "alpha offset".
218        */
219
220        // log_height -> (alpha offset, reduced openings column)
221        let mut reduced_openings: BTreeMap<usize, (Challenge, Vec<Challenge>)> = BTreeMap::new();
222
223        rounds
224            .iter()
225            .zip(values.iter())
226            .for_each(|((data, points_for_mats), values)| {
227                let mats = self.mmcs.get_matrices(data);
228                izip!(mats, points_for_mats, values).for_each(|(mat, points_for_mat, values)| {
229                    let log_height = log2_strict_usize(mat.height());
230                    // It was committed in cfft order.
231                    let evals = CircleEvaluations::from_cfft_order(
232                        CircleDomain::standard(log_height),
233                        mat.as_view(),
234                    );
235
236                    let (alpha_offset, reduced_opening_for_log_height) =
237                        reduced_openings.entry(log_height).or_insert_with(|| {
238                            (Challenge::ONE, vec![Challenge::ZERO; 1 << log_height])
239                        });
240
241                    points_for_mat
242                        .iter()
243                        .zip(values.iter())
244                        .for_each(|(&zeta, ps_at_zeta)| {
245                            let zeta = Point::from_projective_line(zeta);
246
247                            // Reduce this matrix, as a deep quotient, into one column with powers of α.
248                            let mat_ros = evals.deep_quotient_reduce(alpha, zeta, ps_at_zeta);
249
250                            // Fold it into our running reduction, offset by alpha_offset.
251                            reduced_opening_for_log_height
252                                .par_iter_mut()
253                                .zip(mat_ros)
254                                .for_each(|(ro, mat_ro)| {
255                                    *ro += *alpha_offset * mat_ro;
256                                });
257
258                            // Update alpha_offset from α^i -> α^(i + 2 * width)
259                            *alpha_offset *= alpha.exp_u64(2 * evals.values.width() as u64);
260                        });
261                });
262            });
263
264        // Iterate over our reduced columns and extract lambda - the multiple of the vanishing polynomial
265        // which may appear in the reduced quotient due to CFFT dimension gap.
266
267        let mut lambdas = vec![];
268        let mut log_heights = vec![];
269        let first_layer_mats: Vec<RowMajorMatrix<Challenge>> = reduced_openings
270            .into_iter()
271            .map(|(log_height, (_, mut ro))| {
272                assert!(log_height > 0);
273                log_heights.push(log_height);
274                let lambda = extract_lambda(&mut ro, self.fri_params.log_blowup);
275                lambdas.push(lambda);
276                // Prepare for first layer fold with 2 siblings per leaf.
277                RowMajorMatrix::new(ro, 2)
278            })
279            .collect();
280        let log_max_height = log_heights.iter().max().copied().unwrap();
281
282        // Commit to reduced openings at each log_height, so we can challenge a global
283        // folding factor for all first layers, which we use for a "manual" (not part of p3-fri) fold.
284        // This is necessary because the first layer of folding uses different twiddles, so it's easiest
285        // to do it here, before p3-fri.
286
287        let (first_layer_commitment, first_layer_data) =
288            self.fri_params.mmcs.commit(first_layer_mats);
289        challenger.observe(first_layer_commitment.clone());
290        let bivariate_beta: Challenge = challenger.sample_algebra_element();
291
292        // Fold all first layers at bivariate_beta.
293
294        let fri_input: Vec<Vec<Challenge>> = self
295            .fri_params
296            .mmcs
297            .get_matrices(&first_layer_data)
298            .into_iter()
299            .map(|m| fold_y(bivariate_beta, m))
300            // Reverse, because FRI expects descending by height
301            .rev()
302            .collect();
303
304        let folding: CircleFriFoldingForMmcs<Val, Challenge, InputMmcs, FriMmcs> =
305            CircleFriFolding(PhantomData);
306
307        let fri_proof = prove(&folding, &self.fri_params, fri_input, challenger, |index| {
308            // CircleFriFolder asks for an extra query index bit, so we use that here to index
309            // the first layer fold.
310
311            // Open the input (big opening, lots of columns) at the full index...
312            let input_openings = rounds
313                .iter()
314                .map(|(data, _)| {
315                    let log_max_batch_height = log2_strict_usize(self.mmcs.get_max_height(data));
316                    let reduced_index = index >> (log_max_height - log_max_batch_height);
317                    self.mmcs.open_batch(reduced_index, data)
318                })
319                .collect();
320
321            // We committed to first_layer in pairs, so open the reduced index and include the sibling
322            // as part of the input proof.
323            let (first_layer_values, first_layer_proof) = self
324                .fri_params
325                .mmcs
326                .open_batch(index >> 1, &first_layer_data)
327                .unpack();
328            let first_layer_siblings = izip!(&first_layer_values, &log_heights)
329                .map(|(v, log_height)| {
330                    let reduced_index = index >> (log_max_height - log_height);
331                    let sibling_index = (reduced_index & 1) ^ 1;
332                    v[sibling_index]
333                })
334                .collect();
335            CircleInputProof {
336                input_openings,
337                first_layer_siblings,
338                first_layer_proof,
339            }
340        });
341
342        (
343            values,
344            CirclePcsProof {
345                first_layer_commitment,
346                lambdas,
347                fri_proof,
348            },
349        )
350    }
351
352    fn verify(
353        &self,
354        // For each round:
355        rounds: Vec<(
356            Self::Commitment,
357            // for each matrix:
358            Vec<(
359                // its domain,
360                Self::Domain,
361                // for each point:
362                Vec<(
363                    // the point,
364                    Challenge,
365                    // values at the point
366                    Vec<Challenge>,
367                )>,
368            )>,
369        )>,
370        proof: &Self::Proof,
371        challenger: &mut Challenger,
372    ) -> Result<(), Self::Error> {
373        // Write evaluations to challenger
374        for (_, round) in &rounds {
375            for (_, mat) in round {
376                for (_, point) in mat {
377                    challenger.observe_algebra_slice(point);
378                }
379            }
380        }
381
382        // Batch combination challenge
383        let alpha: Challenge = challenger.sample_algebra_element();
384        challenger.observe(proof.first_layer_commitment.clone());
385        let bivariate_beta: Challenge = challenger.sample_algebra_element();
386
387        // +1 to account for first layer
388        let log_global_max_height =
389            proof.fri_proof.commit_phase_commits.len() + self.fri_params.log_blowup + 1;
390
391        let folding: CircleFriFoldingForMmcs<Val, Challenge, InputMmcs, FriMmcs> =
392            CircleFriFolding(PhantomData);
393
394        verify(
395            &folding,
396            &self.fri_params,
397            &proof.fri_proof,
398            challenger,
399            |index, input_proof| {
400                // log_height -> (alpha_offset, ro)
401                let mut reduced_openings = BTreeMap::new();
402
403                let CircleInputProof {
404                    input_openings,
405                    first_layer_siblings,
406                    first_layer_proof,
407                } = input_proof;
408
409                for (batch_opening, (batch_commit, mats)) in
410                    zip_eq(input_openings, &rounds, InputError::InputShapeError)?
411                {
412                    let batch_heights: Vec<usize> = mats
413                        .iter()
414                        .map(|(domain, _)| domain.size() << self.fri_params.log_blowup)
415                        .collect_vec();
416                    let batch_dims: Vec<Dimensions> = batch_heights
417                        .iter()
418                        // todo: mmcs doesn't really need width
419                        .map(|&height| Dimensions { width: 0, height })
420                        .collect_vec();
421
422                    let (dims, idx) = batch_heights
423                        .iter()
424                        .max()
425                        .map(|x| log2_strict_usize(*x))
426                        .map_or_else(
427                            ||
428                            // Empty batch?
429                            (&[][..], 0),
430                            |log_batch_max_height| {
431                                (
432                                    &batch_dims[..],
433                                    index >> (log_global_max_height - log_batch_max_height),
434                                )
435                            },
436                        );
437
438                    self.mmcs
439                        .verify_batch(batch_commit, dims, idx, batch_opening.into())
440                        .map_err(InputError::InputMmcsError)?;
441
442                    for (ps_at_x, (mat_domain, mat_points_and_values)) in zip_eq(
443                        &batch_opening.opened_values,
444                        mats,
445                        InputError::InputShapeError,
446                    )? {
447                        let log_height = mat_domain.log_n + self.fri_params.log_blowup;
448                        let bits_reduced = log_global_max_height - log_height;
449                        let orig_idx = cfft_permute_index(index >> bits_reduced, log_height);
450
451                        let committed_domain = CircleDomain::standard(log_height);
452                        let x = committed_domain.nth_point(orig_idx);
453
454                        let (alpha_offset, ro) = reduced_openings
455                            .entry(log_height)
456                            .or_insert((Challenge::ONE, Challenge::ZERO));
457                        let alpha_pow_width_2 = alpha.exp_u64(ps_at_x.len() as u64).square();
458
459                        for (zeta_uni, ps_at_zeta) in mat_points_and_values {
460                            let zeta = Point::from_projective_line(*zeta_uni);
461
462                            *ro += *alpha_offset
463                                * deep_quotient_reduce_row(alpha, x, zeta, ps_at_x, ps_at_zeta);
464
465                            *alpha_offset *= alpha_pow_width_2;
466                        }
467                    }
468                }
469
470                // Verify bivariate fold and lambda correction
471
472                let (mut fri_input, fl_dims, fl_leaves): (Vec<_>, Vec<_>, Vec<_>) = zip_eq(
473                    zip_eq(
474                        reduced_openings,
475                        first_layer_siblings,
476                        InputError::InputShapeError,
477                    )?,
478                    &proof.lambdas,
479                    InputError::InputShapeError,
480                )?
481                .map(|(((log_height, (_, ro)), &fl_sib), &lambda)| {
482                    assert!(log_height > 0);
483
484                    let orig_size = log_height - self.fri_params.log_blowup;
485                    let bits_reduced = log_global_max_height - log_height;
486                    let orig_idx = cfft_permute_index(index >> bits_reduced, log_height);
487
488                    let lde_domain = CircleDomain::standard(log_height);
489                    let p: Point<Val> = lde_domain.nth_point(orig_idx);
490
491                    let lambda_corrected = ro - lambda * p.v_n(orig_size);
492
493                    let mut fl_values = vec![lambda_corrected; 2];
494                    fl_values[((index >> bits_reduced) & 1) ^ 1] = fl_sib;
495
496                    let fri_input = (
497                        // - 1 here is because we have already folded a layer.
498                        log_height - 1,
499                        fold_y_row(
500                            index >> (bits_reduced + 1),
501                            // - 1 here is log_arity.
502                            log_height - 1,
503                            bivariate_beta,
504                            fl_values.iter().copied(),
505                        ),
506                    );
507
508                    let fl_dims = Dimensions {
509                        width: 0,
510                        height: 1 << (log_height - 1),
511                    };
512
513                    (fri_input, fl_dims, fl_values)
514                })
515                .multiunzip();
516
517                // sort descending
518                fri_input.reverse();
519
520                self.fri_params
521                    .mmcs
522                    .verify_batch(
523                        &proof.first_layer_commitment,
524                        &fl_dims,
525                        index >> 1,
526                        BatchOpeningRef::new(&fl_leaves, first_layer_proof),
527                    )
528                    .map_err(InputError::FirstLayerMmcsError)?;
529
530                Ok(fri_input)
531            },
532        )
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use p3_challenger::{HashChallenger, SerializingChallenger32};
539    use p3_commit::ExtensionMmcs;
540    use p3_field::extension::BinomialExtensionField;
541    use p3_fri::create_test_fri_params;
542    use p3_keccak::Keccak256Hash;
543    use p3_merkle_tree::MerkleTreeMmcs;
544    use p3_mersenne_31::Mersenne31;
545    use p3_symmetric::{CompressionFunctionFromHasher, SerializingHasher};
546    use rand::rngs::SmallRng;
547    use rand::{Rng, SeedableRng};
548
549    use super::*;
550
551    #[test]
552    fn circle_pcs() {
553        // Very simple pcs test. More rigorous tests in p3_fri/tests/pcs.
554
555        let mut rng = SmallRng::seed_from_u64(0);
556
557        type Val = Mersenne31;
558        type Challenge = BinomialExtensionField<Mersenne31, 3>;
559
560        type ByteHash = Keccak256Hash;
561        type FieldHash = SerializingHasher<ByteHash>;
562        let byte_hash = ByteHash {};
563        let field_hash = FieldHash::new(byte_hash);
564
565        type MyCompress = CompressionFunctionFromHasher<ByteHash, 2, 32>;
566        let compress = MyCompress::new(byte_hash);
567
568        type ValMmcs = MerkleTreeMmcs<Val, u8, FieldHash, MyCompress, 32>;
569        let val_mmcs = ValMmcs::new(field_hash, compress);
570
571        type ChallengeMmcs = ExtensionMmcs<Val, Challenge, ValMmcs>;
572        let challenge_mmcs = ChallengeMmcs::new(val_mmcs.clone());
573
574        type Challenger = SerializingChallenger32<Val, HashChallenger<u8, ByteHash, 32>>;
575
576        let fri_params = create_test_fri_params(challenge_mmcs, 0);
577
578        type Pcs = CirclePcs<Val, ValMmcs, ChallengeMmcs>;
579        let pcs = Pcs {
580            mmcs: val_mmcs,
581            fri_params,
582            _phantom: PhantomData,
583        };
584
585        let log_n = 10;
586
587        let d = <Pcs as p3_commit::Pcs<Challenge, Challenger>>::natural_domain_for_degree(
588            &pcs,
589            1 << log_n,
590        );
591
592        let evals = RowMajorMatrix::rand(&mut rng, 1 << log_n, 1);
593
594        let (comm, data) =
595            <Pcs as p3_commit::Pcs<Challenge, Challenger>>::commit(&pcs, [(d, evals)]);
596
597        let zeta: Challenge = rng.random();
598
599        let mut chal = Challenger::from_hasher(vec![], byte_hash);
600        let (values, proof) = pcs.open(vec![(&data, vec![vec![zeta]])], &mut chal);
601
602        let mut chal = Challenger::from_hasher(vec![], byte_hash);
603        pcs.verify(
604            vec![(comm, vec![(d, vec![(zeta, values[0][0][0].clone())])])],
605            &proof,
606            &mut chal,
607        )
608        .expect("verify err");
609    }
610}