p3_fri/
hiding_pcs.rs

1use alloc::vec::Vec;
2use core::cell::RefCell;
3
4use itertools::Itertools;
5use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger};
6use p3_commit::{BatchOpening, Mmcs, OpenedValues, Pcs, PolynomialSpace};
7use p3_dft::TwoAdicSubgroupDft;
8use p3_field::coset::TwoAdicMultiplicativeCoset;
9use p3_field::{ExtensionField, Field, TwoAdicField, batch_multiplicative_inverse};
10use p3_matrix::Matrix;
11use p3_matrix::bitrev::{BitReversalPerm, BitReversibleMatrix};
12use p3_matrix::dense::{DenseMatrix, RowMajorMatrix, RowMajorMatrixView};
13use p3_matrix::horizontally_truncated::HorizontallyTruncated;
14use p3_matrix::row_index_mapped::RowIndexMappedView;
15use p3_util::zip_eq::zip_eq;
16use rand::Rng;
17use rand::distr::{Distribution, StandardUniform};
18use tracing::{info_span, instrument};
19
20use crate::verifier::FriError;
21use crate::{FriParameters, FriProof, TwoAdicFriPcs};
22
23/// A hiding FRI PCS. Both MMCSs must also be hiding; this is not enforced at compile time so it's
24/// the user's responsibility to configure.
25#[derive(Clone, Debug)]
26pub struct HidingFriPcs<Val, Dft, InputMmcs, FriMmcs, R> {
27    inner: TwoAdicFriPcs<Val, Dft, InputMmcs, FriMmcs>,
28    num_random_codewords: usize,
29    rng: RefCell<R>,
30}
31
32impl<Val, Dft, InputMmcs, FriMmcs, R> HidingFriPcs<Val, Dft, InputMmcs, FriMmcs, R> {
33    pub fn new(
34        dft: Dft,
35        mmcs: InputMmcs,
36        params: FriParameters<FriMmcs>,
37        num_random_codewords: usize,
38        rng: R,
39    ) -> Self {
40        let inner = TwoAdicFriPcs::new(dft, mmcs, params);
41        Self {
42            inner,
43            num_random_codewords,
44            rng: rng.into(),
45        }
46    }
47}
48
49impl<Val, Dft, InputMmcs, FriMmcs, Challenge, Challenger, R> Pcs<Challenge, Challenger>
50    for HidingFriPcs<Val, Dft, InputMmcs, FriMmcs, R>
51where
52    Val: TwoAdicField,
53    StandardUniform: Distribution<Val>,
54    Dft: TwoAdicSubgroupDft<Val>,
55    InputMmcs: Mmcs<Val>,
56    FriMmcs: Mmcs<Challenge>,
57    Challenge: TwoAdicField + ExtensionField<Val>,
58    Challenger:
59        FieldChallenger<Val> + CanObserve<FriMmcs::Commitment> + GrindingChallenger<Witness = Val>,
60    R: Rng + Send + Sync,
61{
62    type Domain = TwoAdicMultiplicativeCoset<Val>;
63    type Commitment = InputMmcs::Commitment;
64    type ProverData = InputMmcs::ProverData<RowMajorMatrix<Val>>;
65    type EvaluationsOnDomain<'a> = HorizontallyTruncated<
66        Val,
67        RowIndexMappedView<BitReversalPerm, RowMajorMatrixView<'a, Val>>,
68    >;
69    /// The first item contains the openings of the random polynomials added by this wrapper.
70    /// The second item is the usual FRI proof.
71    type Proof = (
72        OpenedValues<Challenge>,
73        FriProof<Challenge, FriMmcs, Val, Vec<BatchOpening<Val, InputMmcs>>>,
74    );
75    type Error = FriError<FriMmcs::Error, InputMmcs::Error>;
76
77    const ZK: bool = true;
78
79    fn natural_domain_for_degree(&self, degree: usize) -> Self::Domain {
80        <TwoAdicFriPcs<Val, Dft, InputMmcs, FriMmcs> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
81            &self.inner, degree)
82    }
83
84    fn commit(
85        &self,
86        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
87    ) -> (Self::Commitment, Self::ProverData) {
88        let randomized_evaluations: Vec<(Self::Domain, RowMajorMatrix<Val>)> =
89            info_span!("randomize polys").in_scope(|| {
90                evaluations
91                    .into_iter()
92                    .map(|(domain, mat)| {
93                        let mat_width = mat.width();
94                        // Let `w` and `h` be the width and height of the original matrix. The randomized matrix should have height `2h` and width `w + num_random_codewords`.
95                        // To generate it, we add `w + 2 * num_random_codewords` columns to the original matrix, then reshape it by setting the width to `w + num_random_codewords`.
96                        // All columns are added on the right hand side so, after reshaping, this has the net effect of adding `num_random_codewords` random columns on the right and interleaving the original trace with random rows.
97
98                        let mut random_evaluation = add_random_cols(
99                            &mat,
100                            mat_width + 2 * self.num_random_codewords,
101                            &mut *self.rng.borrow_mut(),
102                        );
103                        random_evaluation.width = mat_width + self.num_random_codewords;
104
105                        (domain, random_evaluation)
106                    })
107                    .collect()
108            });
109
110        Pcs::<Challenge, Challenger>::commit(&self.inner, randomized_evaluations)
111    }
112
113    fn commit_preprocessing(
114        &self,
115        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
116    ) -> (Self::Commitment, Self::ProverData) {
117        // Pad values with zero columns instead of random columns.
118        let padded_evals = evaluations
119            .into_iter()
120            .map(|(domain, mat)| {
121                let mat_width = mat.width();
122                // Let `w` and `h` be the width and height of the original matrix. The padded matrix should have height `2h` and width `w`.
123                // To generate it, we add `w` zero columns to the original matrix, then reshape it by setting the width to `w`.
124                // All columns are added on the right hand side so, after reshaping, this has the net effect of adding interleaving the original trace with zero rows.
125                let mut padded_evaluation = add_zero_cols(&mat, mat_width);
126                padded_evaluation.width = mat_width;
127                (domain, padded_evaluation)
128            })
129            .collect::<Vec<_>>();
130
131        Pcs::<Challenge, Challenger>::commit(&self.inner, padded_evals)
132    }
133
134    /// Get the quotient polynomial LDEs. We first decompose the quotient polynomial into
135    /// `num_chunks` many smaller polynomials each of degree `degree / num_chunks`.
136    /// These quotient polynomials are then randomized as explained in Section 4.2 of
137    /// https://eprint.iacr.org/2024/1037.pdf .
138    ///
139    /// ### Arguments
140    /// - `quotient_domain` the domain of the quotient polynomial.
141    /// - `quotient_evaluations` the evaluations of the quotient polynomial over the domain. This should be in
142    ///   standard (not bit-reversed) order.
143    /// - `num_chunks` the number of smaller polynomials to decompose the quotient polynomial into.
144    ///
145    /// # Panics
146    /// This function panics if `num_chunks` is either `0` or `1`. The first case makes no logical
147    /// sense and in the second case, the resulting commitment would not be hiding.
148    fn get_quotient_ldes(
149        &self,
150        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
151        num_chunks: usize,
152    ) -> Vec<RowMajorMatrix<Val>> {
153        let (domains, evaluations): (Vec<_>, Vec<_>) = evaluations.into_iter().unzip();
154        let cis = get_zp_cis(&domains);
155        let last_chunk = num_chunks - 1;
156        let last_chunk_ci_inv = cis[last_chunk].inverse();
157        let mul_coeffs = (0..last_chunk)
158            .map(|i| cis[i] * last_chunk_ci_inv)
159            .collect_vec();
160
161        let mut rng = self.rng.borrow_mut();
162        let randomized_evaluations: Vec<RowMajorMatrix<Val>> = evaluations
163            .into_iter()
164            .map(|mat| add_random_cols(&mat, self.num_random_codewords, &mut *rng))
165            .collect();
166        // Add random values to the LDE evaluations as described in https://eprint.iacr.org/2024/1037.pdf.
167        // If we have `d` chunks, let q'_i(X) = q_i(X) + v_H_i(X) * t_i(X) where t(X) is random, for 1 <= i < d.
168        // q'_d(X) = q_d(X) - v_H_d(X) c_i \sum t_i(X) where c_i is a Lagrange normalization constant.
169        let h = randomized_evaluations[0].height();
170        let w = randomized_evaluations[0].width();
171        let mut all_random_values = (0..(randomized_evaluations.len() - 1) * h * w)
172            .map(|_| rng.random())
173            .chain(core::iter::repeat_n(Val::ZERO, h * w))
174            .collect::<Vec<_>>();
175
176        // Set the random values for the final chunk accordingly
177        for j in 0..last_chunk {
178            let mul_coeff = mul_coeffs[j];
179            for k in 0..h * w {
180                let t = all_random_values[j * h * w + k] * mul_coeff;
181                all_random_values[last_chunk * h * w + k] -= t;
182            }
183        }
184
185        domains
186            .into_iter()
187            .zip(randomized_evaluations)
188            .enumerate()
189            .map(|(i, (domain, evals))| {
190                assert_eq!(domain.size(), evals.height());
191                let shift = Val::GENERATOR / domain.shift();
192                let random_values = &all_random_values[i * h * w..(i + 1) * h * w];
193
194                // Commit to the bit-reversed LDE.
195                let mut lde_evals = self
196                    .inner
197                    .dft
198                    .coset_lde_batch(evals, self.inner.fri.log_blowup + 1, shift)
199                    .to_row_major_matrix();
200
201                // Evaluate `v_H(X) * r(X)` over the LDE, where:
202                // - `v_H` is the coset vanishing polynomial, here equal to (GENERATOR * X / domain.shift)^n - 1,
203                // - and `r` is a random polynomial.
204                let mut vanishing_poly_coeffs =
205                    Val::zero_vec((h * w) << (self.inner.fri.log_blowup + 1));
206                let p = shift.exp_u64(h as u64);
207                Val::GENERATOR
208                    .powers()
209                    .take(h)
210                    .enumerate()
211                    .for_each(|(i, p_i)| {
212                        for j in 0..w {
213                            let mul_coeff = p_i * random_values[i * w + j];
214                            vanishing_poly_coeffs[i * w + j] -= mul_coeff;
215                            vanishing_poly_coeffs[(h + i) * w + j] = p * mul_coeff;
216                        }
217                    });
218                let random_eval = self
219                    .inner
220                    .dft
221                    .dft_batch(DenseMatrix::new(vanishing_poly_coeffs, w))
222                    .to_row_major_matrix();
223
224                // Add the quotient chunk evaluations over the LDE to the evaluations of `v_H(X) * r(X)`.
225                for i in 0..h * w * (1 << (self.inner.fri.log_blowup + 1)) {
226                    lde_evals.values[i] += random_eval.values[i];
227                }
228
229                lde_evals.bit_reverse_rows().to_row_major_matrix()
230            })
231            .collect()
232    }
233
234    fn commit_ldes(&self, ldes: Vec<RowMajorMatrix<Val>>) -> (Self::Commitment, Self::ProverData) {
235        Pcs::<Challenge, Challenger>::commit_ldes(&self.inner, ldes)
236    }
237
238    fn get_evaluations_on_domain<'a>(
239        &self,
240        prover_data: &'a Self::ProverData,
241        idx: usize,
242        domain: Self::Domain,
243    ) -> Self::EvaluationsOnDomain<'a> {
244        let inner_evals = <TwoAdicFriPcs<Val, Dft, InputMmcs, FriMmcs> as Pcs<
245            Challenge,
246            Challenger,
247        >>::get_evaluations_on_domain(
248            &self.inner, prover_data, idx, domain
249        );
250        let inner_width = inner_evals.width();
251        // Truncate off the columns representing random codewords we added in `commit` above.
252        // The unwrap is safe as inner_width - self.num_random_codewords <= inner_width.
253        HorizontallyTruncated::new(inner_evals, inner_width - self.num_random_codewords).unwrap()
254    }
255
256    fn get_evaluations_on_domain_no_random<'a>(
257        &self,
258        prover_data: &'a Self::ProverData,
259        idx: usize,
260        domain: Self::Domain,
261    ) -> Self::EvaluationsOnDomain<'a> {
262        let inner_evals = <TwoAdicFriPcs<Val, Dft, InputMmcs, FriMmcs> as Pcs<
263            Challenge,
264            Challenger,
265        >>::get_evaluations_on_domain(
266            &self.inner, prover_data, idx, domain
267        );
268        let inner_width = inner_evals.width();
269
270        HorizontallyTruncated::new(inner_evals, inner_width).unwrap()
271    }
272
273    fn open(
274        &self,
275        // For each round,
276        rounds: Vec<(
277            &Self::ProverData,
278            // for each matrix,
279            Vec<
280                // points to open
281                Vec<Challenge>,
282            >,
283        )>,
284        challenger: &mut Challenger,
285    ) -> (OpenedValues<Challenge>, Self::Proof) {
286        self.open_with_preprocessing(rounds, challenger, false)
287    }
288
289    fn open_with_preprocessing(
290        &self,
291        // For each round,
292        rounds: Vec<(
293            &Self::ProverData,
294            // for each matrix,
295            Vec<
296                // points to open
297                Vec<Challenge>,
298            >,
299        )>,
300        challenger: &mut Challenger,
301        is_preprocessing: bool,
302    ) -> (OpenedValues<Challenge>, Self::Proof) {
303        let (mut inner_opened_values, inner_proof) =
304            self.inner
305                .open_with_preprocessing(rounds, challenger, is_preprocessing);
306        // inner_opened_values includes opened values for the random codewords. Those should be
307        // hidden from our caller, so we split them off and store them in the proof.
308        let opened_values_rand = inner_opened_values
309            .iter_mut()
310            .enumerate()
311            .map(|(idx, opened_values_for_round)| {
312                opened_values_for_round
313                    .iter_mut()
314                    .map(|opened_values_for_mat| {
315                        opened_values_for_mat
316                            .iter_mut()
317                            .map(|opened_values_for_point| {
318                                let num_random_codewords =
319                                    if is_preprocessing && idx == <Self as Pcs<Challenge, Challenger>>::PREPROCESSED_TRACE_IDX {
320                                        0
321                                    } else {
322                                        self.num_random_codewords
323                                    };
324                                let split = opened_values_for_point.len() - num_random_codewords;
325                                opened_values_for_point.drain(split..).collect()
326                            })
327                            .collect()
328                    })
329                    .collect()
330            })
331            .collect();
332
333        (inner_opened_values, (opened_values_rand, inner_proof))
334    }
335
336    fn verify(
337        &self,
338        // For each round:
339        mut rounds: Vec<(
340            Self::Commitment,
341            // for each matrix:
342            Vec<(
343                // its domain,
344                Self::Domain,
345                // for each point:
346                Vec<(
347                    // the point,
348                    Challenge,
349                    // values at the point
350                    Vec<Challenge>,
351                )>,
352            )>,
353        )>,
354        proof: &Self::Proof,
355        challenger: &mut Challenger,
356    ) -> Result<(), Self::Error> {
357        let (opened_values_for_rand_cws, inner_proof) = proof;
358        // Now we merge `opened_values_for_rand_cws` into the opened values in `rounds`, undoing
359        // the split that we did in `open`, to get a complete set of opened values for the inner PCS
360        // to check.
361        for (round, rand_round) in zip_eq(
362            rounds.iter_mut(),
363            opened_values_for_rand_cws,
364            FriError::InvalidProofShape,
365        )? {
366            for (mat, rand_mat) in
367                zip_eq(round.1.iter_mut(), rand_round, FriError::InvalidProofShape)?
368            {
369                for (point, rand_point) in
370                    zip_eq(mat.1.iter_mut(), rand_mat, FriError::InvalidProofShape)?
371                {
372                    point.1.extend(rand_point);
373                }
374            }
375        }
376        self.inner.verify(rounds, inner_proof, challenger)
377    }
378
379    fn get_opt_randomization_poly_commitment(
380        &self,
381        ext_trace_domains: impl IntoIterator<Item = Self::Domain>,
382    ) -> Option<(Self::Commitment, Self::ProverData)> {
383        let random_input_vals = ext_trace_domains
384            .into_iter()
385            .map(|domain| {
386                let m = DenseMatrix::rand(
387                    &mut *self.rng.borrow_mut(),
388                    domain.size(),
389                    self.num_random_codewords + Challenge::DIMENSION,
390                );
391
392                (domain, m)
393            })
394            .collect::<Vec<_>>();
395
396        let r_commit_and_data =
397            Pcs::<Challenge, Challenger>::commit(&self.inner, random_input_vals);
398        Some(r_commit_and_data)
399    }
400}
401
402#[instrument(level = "debug", skip_all)]
403fn add_random_cols<Val, R>(
404    mat: &RowMajorMatrix<Val>,
405    num_random_codewords: usize,
406    mut rng: R,
407) -> RowMajorMatrix<Val>
408where
409    Val: Field,
410    R: Rng + Send + Sync,
411    StandardUniform: Distribution<Val>,
412{
413    let old_w = mat.width();
414    let new_w = old_w + num_random_codewords;
415    let h = mat.height();
416
417    let new_values = Val::zero_vec(new_w * h);
418    let mut result = RowMajorMatrix::new(new_values, new_w);
419    // Can be parallelized by adding par_, but there are some complications with the RNG.
420    // We could just use rng(), but ideally we want to keep it generic...
421    result
422        .rows_mut()
423        .zip(mat.row_slices())
424        .for_each(|(new_row, old_row)| {
425            new_row[..old_w].copy_from_slice(old_row);
426            new_row[old_w..].iter_mut().for_each(|v| *v = rng.random());
427        });
428    result
429}
430
431/// Adds `num_extra_columns` zero columns to the right of `mat`, then reshapes it by setting the width to
432/// `mat.width() + num_extra_columns`.
433#[instrument(level = "debug", skip_all)]
434fn add_zero_cols<Val>(mat: &RowMajorMatrix<Val>, num_extra_columns: usize) -> RowMajorMatrix<Val>
435where
436    Val: Field,
437{
438    let old_w = mat.width();
439    let new_w = old_w + num_extra_columns;
440    let h = mat.height();
441
442    let new_values = Val::zero_vec(new_w * h);
443    let mut result = RowMajorMatrix::new(new_values, new_w);
444
445    result
446        .rows_mut()
447        .zip(mat.row_slices())
448        .for_each(|(new_row, old_row)| {
449            new_row[..old_w].copy_from_slice(old_row);
450        });
451    result
452}
453
454/// Compute the normalizing constants for the Langrange selectors of the provided domains.
455/// See Section 4.2 of https://eprint.iacr.org/2024/1037.pdf for more details.
456fn get_zp_cis<D: PolynomialSpace>(qc_domains: &[D]) -> Vec<p3_commit::Val<D>> {
457    batch_multiplicative_inverse(
458        &qc_domains
459            .iter()
460            .enumerate()
461            .map(|(i, domain)| {
462                qc_domains
463                    .iter()
464                    .enumerate()
465                    .filter(|(j, _)| *j != i)
466                    .map(|(_, other_domain)| {
467                        other_domain.vanishing_poly_at_point(domain.first_point())
468                    })
469                    .product()
470            })
471            .collect::<Vec<_>>(),
472    )
473}