p3_circle/
pcs.rs

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