p3_fri/
hiding_pcs.rs

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