p3_fri/
verifier.rs

1use alloc::collections::btree_map::BTreeMap;
2use alloc::vec;
3use alloc::vec::Vec;
4
5use itertools::Itertools;
6use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger};
7use p3_commit::{BatchOpening, BatchOpeningRef, Mmcs};
8use p3_field::coset::TwoAdicMultiplicativeCoset;
9use p3_field::{ExtensionField, Field, TwoAdicField};
10use p3_matrix::Dimensions;
11use p3_util::zip_eq::zip_eq;
12use p3_util::{log2_strict_usize, reverse_bits_len};
13use thiserror::Error;
14
15use crate::{
16    CommitPhaseProofStep, CommitmentWithOpeningPoints, FriFoldingStrategy, FriParameters, FriProof,
17    QueryProof,
18};
19
20#[derive(Debug, Error)]
21pub enum FriError<CommitMmcsErr, InputError>
22where
23    CommitMmcsErr: core::fmt::Debug,
24    InputError: core::fmt::Debug,
25{
26    #[error("invalid proof shape")]
27    InvalidProofShape,
28    #[error("commit phase MMCS error: {0:?}")]
29    CommitPhaseMmcsError(CommitMmcsErr),
30    #[error("input error: {0:?}")]
31    InputError(InputError),
32    #[error("final polynomial mismatch: evaluation does not match expected value")]
33    FinalPolyMismatch,
34    #[error("invalid proof-of-work witness")]
35    InvalidPowWitness,
36    #[error("missing input: required input is not present")]
37    MissingInput,
38}
39
40/// A chain of FRI input openings allowing a verifier to check a sequence of
41/// FRI folds and rolls. The first element of each pair indicates the round of
42/// fri in which the input should be rolled in. The second element is the opening.
43type FriOpenings<F> = Vec<(usize, F)>;
44
45/// Verifies a FRI proof.
46///
47/// Arguments:
48/// - `folding`: The FRI folding scheme used by the prover.
49/// - `params`: The parameters for the specific FRI protocol instance.
50/// - `proof`: The proof to verify.
51/// - `challenger`: The Fiat-Shamir challenger.
52/// - `commitments_with_opening_points`: A vector of joint commitments to collections of matrices
53///   and openings of those matrices at a collection of points.
54pub fn verify_fri<Folding, Val, Challenge, InputMmcs, FriMmcs, Challenger>(
55    folding: &Folding,
56    params: &FriParameters<FriMmcs>,
57    proof: &FriProof<Challenge, FriMmcs, Challenger::Witness, Folding::InputProof>,
58    challenger: &mut Challenger,
59    commitments_with_opening_points: &[CommitmentWithOpeningPoints<
60        Challenge,
61        InputMmcs::Commitment,
62        TwoAdicMultiplicativeCoset<Val>,
63    >],
64    input_mmcs: &InputMmcs,
65) -> Result<(), FriError<FriMmcs::Error, InputMmcs::Error>>
66where
67    Val: TwoAdicField,
68    Challenge: ExtensionField<Val>,
69    InputMmcs: Mmcs<Val>,
70    FriMmcs: Mmcs<Challenge>,
71    Challenger: FieldChallenger<Val> + GrindingChallenger + CanObserve<FriMmcs::Commitment>,
72    Folding: FriFoldingStrategy<
73            Val,
74            Challenge,
75            InputError = InputMmcs::Error,
76            InputProof = Vec<BatchOpening<Val, InputMmcs>>,
77        >,
78{
79    // Generate the Batch combination challenge
80    // Soundness Error: `|f|/|EF|` where `|f|` is the number of different functions of the form
81    // `(f(zeta) - fi(x))/(zeta - x)` which need to be checked.
82    // Explicitly, `|f|` is `commitments_with_opening_points.flatten().flatten().len()`
83    // (i.e counting the number (point, claimed_evaluation) pairs).
84    let alpha: Challenge = challenger.sample_algebra_element();
85
86    // `commit_phase_commits.len()` is the number of folding steps, so the maximum polynomial degree will be
87    // `commit_phase_commits.len() + self.fri.log_final_poly_len` and so, as the same blow-up is used for all
88    // polynomials, the maximum matrix height over all commit batches is:
89    let log_global_max_height =
90        proof.commit_phase_commits.len() + params.log_blowup + params.log_final_poly_len;
91
92    if proof.commit_pow_witnesses.len() != proof.commit_phase_commits.len() {
93        return Err(FriError::InvalidProofShape);
94    }
95
96    // Generate all of the random challenges for the FRI rounds, checking PoW per round.
97    let betas: Vec<Challenge> = proof
98        .commit_phase_commits
99        .iter()
100        .zip(&proof.commit_pow_witnesses)
101        .map(|(comm, witness)| {
102            // Observe the commitment, check the PoW witness, then sample the
103            // folding challenge.
104            challenger.observe(comm.clone());
105            if !challenger.check_witness(params.commit_proof_of_work_bits, *witness) {
106                return Err(FriError::InvalidPowWitness);
107            }
108            Ok(challenger.sample_algebra_element())
109        })
110        .collect::<Result<Vec<_>, _>>()?;
111
112    // Ensure that the final polynomial has the expected degree.
113    if proof.final_poly.len() != params.final_poly_len() {
114        return Err(FriError::InvalidProofShape);
115    }
116
117    // Observe all coefficients of the final polynomial.
118    challenger.observe_algebra_slice(&proof.final_poly);
119
120    // Ensure that we have the expected number of FRI query proofs.
121    if proof.query_proofs.len() != params.num_queries {
122        return Err(FriError::InvalidProofShape);
123    }
124
125    // Check PoW.
126    if !challenger.check_witness(params.query_proof_of_work_bits, proof.query_pow_witness) {
127        return Err(FriError::InvalidPowWitness);
128    }
129
130    // The log of the final domain size.
131    let log_final_height = params.log_blowup + params.log_final_poly_len;
132
133    for QueryProof {
134        input_proof,
135        commit_phase_openings,
136    } in &proof.query_proofs
137    {
138        // For each query proof, we start by generating the random index.
139        let index =
140            challenger.sample_bits(log_global_max_height + folding.extra_query_index_bits());
141
142        // Next we open all polynomials `f` at the relevant index and combine them into our FRI inputs.
143        let ro = open_input(
144            params,
145            log_global_max_height,
146            index,
147            input_proof,
148            alpha,
149            input_mmcs,
150            commitments_with_opening_points,
151        )?;
152
153        debug_assert!(
154            ro.iter().tuple_windows().all(|((l, _), (r, _))| l > r),
155            "reduced openings sorted by height descending"
156        );
157
158        // If we queried extra bits, shift them off now.
159        let mut domain_index = index >> folding.extra_query_index_bits();
160
161        // Starting at the evaluation at `index` of the initial domain,
162        // perform FRI folds until the domain size reaches the final domain size.
163        // Check after each fold that the pair of sibling evaluations at the current
164        // node match the commitment.
165        let folded_eval = verify_query(
166            folding,
167            params,
168            &mut domain_index,
169            zip_eq(
170                zip_eq(
171                    &betas,
172                    &proof.commit_phase_commits,
173                    FriError::InvalidProofShape,
174                )?,
175                commit_phase_openings,
176                FriError::InvalidProofShape,
177            )?,
178            ro,
179            log_global_max_height,
180            log_final_height,
181        )?;
182
183        // We open the final polynomial at index `domain_index`, which corresponds to evaluating
184        // the polynomial at x^k, where x is the 2-adic generator of order `max_height` and k is
185        // `reverse_bits_len(domain_index, log_global_max_height)`.
186        let x = Val::two_adic_generator(log_global_max_height)
187            .exp_u64(reverse_bits_len(domain_index, log_global_max_height) as u64);
188
189        // Assuming all the checks passed, the final check is to ensure that the folded evaluation
190        // matches the evaluation of the final polynomial sent by the prover.
191
192        // Evaluate the final polynomial at x.
193        let mut eval = Challenge::ZERO;
194        for &coeff in proof.final_poly.iter().rev() {
195            eval = eval * x + coeff;
196        }
197
198        if eval != folded_eval {
199            return Err(FriError::FinalPolyMismatch);
200        }
201    }
202
203    Ok(())
204}
205
206type CommitStep<'a, F, M> = (
207    (
208        &'a F, // The challenge point beta used for the next fold of FRI evaluations.
209        &'a <M as Mmcs<F>>::Commitment, // A commitment to the FRI evaluations on the current domain.
210    ),
211    &'a CommitPhaseProofStep<F, M>, // The sibling and opening proof for the current FRI node.
212);
213
214/// Verifies a single query chain in the FRI proof. This is the verifier complement
215/// to the prover's [`answer_query`] function.
216///
217/// Given an initial `index` corresponding to a point in the initial domain
218/// and a series of `reduced_openings` corresponding to evaluations of
219/// polynomials to be added in at specific domain sizes, perform the standard
220/// sequence of FRI folds, checking at each step that the pair of sibling evaluations
221/// matches the commitment.
222///
223/// Arguments:
224/// - `folding`: The FRI folding scheme used by the prover.
225/// - `params`: The parameters for the specific FRI protocol instance.
226/// - `start_index`: The opening index for the unfolded polynomial. For folded polynomials
227///   we use this this index right shifted by the number of folds.
228/// - `fold_data_iter`: An iterator containing, for each fold, the beta challenge, polynomial commitment
229///   and commitment opening at the appropriate index.
230/// - `reduced_openings`: A vector of pairs of a size and an opening. The opening is a linear combination
231///   of all input polynomials of that size opened at the appropriate index. Each opening is added into the
232///   the FRI folding chain once the domain size reaches the size specified in the pair.
233/// - `log_global_max_height`: The log of the maximum domain size.
234/// - `log_final_height`: The log of the final domain size.
235#[inline]
236fn verify_query<'a, Folding, F, EF, M>(
237    folding: &Folding,
238    params: &FriParameters<M>,
239    start_index: &mut usize,
240    fold_data_iter: impl ExactSizeIterator<Item = CommitStep<'a, EF, M>>,
241    reduced_openings: FriOpenings<EF>,
242    log_global_max_height: usize,
243    log_final_height: usize,
244) -> Result<EF, FriError<M::Error, Folding::InputError>>
245where
246    F: Field,
247    EF: ExtensionField<F>,
248    M: Mmcs<EF> + 'a,
249    Folding: FriFoldingStrategy<F, EF>,
250{
251    let mut ro_iter = reduced_openings.into_iter().peekable();
252
253    // These checks are not essential to security,
254    // but they should be satisfied by any non malicious prover.
255    // ro_iter being empty means that we have committed to no polynomials at all and
256    // we need to roll in a polynomial initially otherwise we are just folding a zero polynomial.
257    if ro_iter.peek().is_none() || ro_iter.peek().unwrap().0 != log_global_max_height {
258        return Err(FriError::InvalidProofShape);
259    }
260    let mut folded_eval = ro_iter.next().unwrap().1;
261
262    // We start with evaluations over a domain of size (1 << log_global_max_height). We fold
263    // using FRI until the domain size reaches (1 << log_final_height).
264    for (log_folded_height, ((&beta, comm), opening)) in zip_eq(
265        // zip_eq ensures that we have the right number of steps.
266        (log_final_height..log_global_max_height).rev(),
267        fold_data_iter,
268        FriError::InvalidProofShape,
269    )? {
270        // Get the index of the other sibling of the current FRI node.
271        let index_sibling = *start_index ^ 1;
272
273        let mut evals = vec![folded_eval; 2];
274        evals[index_sibling % 2] = opening.sibling_value;
275
276        let dims = &[Dimensions {
277            width: 2,
278            height: 1 << log_folded_height,
279        }];
280
281        // Replace index with the index of the parent FRI node.
282        *start_index >>= 1;
283
284        // Verify the commitment to the evaluations of the sibling nodes.
285        params
286            .mmcs
287            .verify_batch(
288                comm,
289                dims,
290                *start_index,
291                BatchOpeningRef::new(&[evals.clone()], &opening.opening_proof), // It's possible to remove the clone here but unnecessary as evals is tiny.
292            )
293            .map_err(FriError::CommitPhaseMmcsError)?;
294
295        // Fold the pair of sibling nodes to get the evaluation of the parent FRI node.
296        folded_eval = folding.fold_row(*start_index, log_folded_height, beta, evals.into_iter());
297
298        // If there are new polynomials to roll in at the folded height, do so.
299        //
300        // Each element of `ro_iter` is the evaluation of a reduced opening polynomial, which is itself
301        // a random linear combination `f_{i, 0}(x) + alpha f_{i, 1}(x) + ...`, but when we add it
302        // to the current folded polynomial evaluation claim, we need to multiply by a new random factor
303        // since `f_{i, 0}` has no leading coefficient.
304        //
305        // We use `beta^2` as the random factor since `beta` is already used in the folding.
306        // This increases the query phase error probability by a negligible amount, and does not change
307        // the required number of FRI queries.
308        if let Some((_, ro)) = ro_iter.next_if(|(lh, _)| *lh == log_folded_height) {
309            folded_eval += beta.square() * ro;
310        }
311    }
312
313    // If ro_iter is not empty, we failed to fold in some polynomial evaluations.
314    if ro_iter.next().is_some() {
315        return Err(FriError::InvalidProofShape);
316    }
317
318    // If we reached this point, we have verified that, starting at the initial index,
319    // the chain of folds has produced folded_eval.
320    Ok(folded_eval)
321}
322
323/// Given an index and a collection of opening proofs, check all opening proofs and combine
324/// the opened values into the FRI inputs along the path specified by the index.
325///
326/// In cases where the maximum height of a batch of matrices is smaller than the
327/// global max height, shift the index down to compensate.
328///
329/// We combine the functions by mapping each function and opening point pair to `(f(z) - f(x))/(z - x)`
330/// and then combining functions of the same degree using the challenge alpha.
331///
332/// ## Arguments:
333/// - `params`: The FRI parameters.
334/// - `log_global_max_height`: The log of the maximum height of the input matrices.
335/// - `index`: The index at which to open the functions.
336/// - `input_proof`: A vector of batch openings with each opening containing a
337///   list of opened values for a collection of matrices along with a batched opening proof.
338/// - `alpha`: The challenge used to combine the functions.
339/// - `input_mmcs`: The input multi-matrix commitment scheme.
340/// - `commitments_with_opening_points`: A vector of joint commitments to collections of matrices
341///   and openings of those matrices at a collection of points.
342#[inline]
343fn open_input<Val, Challenge, InputMmcs, FriMmcs>(
344    params: &FriParameters<FriMmcs>,
345    log_global_max_height: usize,
346    index: usize,
347    input_proof: &[BatchOpening<Val, InputMmcs>],
348    alpha: Challenge,
349    input_mmcs: &InputMmcs,
350    commitments_with_opening_points: &[CommitmentWithOpeningPoints<
351        Challenge,
352        InputMmcs::Commitment,
353        TwoAdicMultiplicativeCoset<Val>,
354    >],
355) -> Result<FriOpenings<Challenge>, FriError<FriMmcs::Error, InputMmcs::Error>>
356where
357    Val: TwoAdicField,
358    Challenge: ExtensionField<Val>,
359    InputMmcs: Mmcs<Val>,
360    FriMmcs: Mmcs<Challenge>,
361{
362    // For each log_height, we store the alpha power and compute the reduced opening.
363    // log_height -> (alpha_pow, reduced_opening)
364    let mut reduced_openings = BTreeMap::<usize, (Challenge, Challenge)>::new();
365
366    // For each batch commitment and opening proof
367    for (batch_opening, (batch_commit, mats)) in zip_eq(
368        input_proof,
369        commitments_with_opening_points,
370        FriError::InvalidProofShape,
371    )? {
372        // Find the height of each matrix in the batch.
373        // Currently we only check domain.size() as the shift is
374        // assumed to always be Val::GENERATOR.
375        let batch_heights = mats
376            .iter()
377            .map(|(domain, _)| domain.size() << params.log_blowup)
378            .collect_vec();
379        let batch_dims = batch_heights
380            .iter()
381            // TODO: MMCS doesn't really need width; we put 0 for now.
382            .map(|&height| Dimensions { width: 0, height })
383            .collect_vec();
384
385        // If the maximum height of the batch is smaller than the global max height,
386        // we need to correct the index by right shifting it.
387        // If the batch is empty, we set the index to 0.
388        let reduced_index = batch_heights
389            .iter()
390            .max()
391            .map(|&h| index >> (log_global_max_height - log2_strict_usize(h)))
392            .unwrap_or(0);
393
394        input_mmcs
395            .verify_batch(
396                batch_commit,
397                &batch_dims,
398                reduced_index,
399                batch_opening.into(),
400            )
401            .map_err(FriError::InputError)?;
402
403        // For each matrix in the commitment
404        for (mat_opening, (mat_domain, mat_points_and_values)) in zip_eq(
405            &batch_opening.opened_values,
406            mats,
407            FriError::InvalidProofShape,
408        )? {
409            let log_height = log2_strict_usize(mat_domain.size()) + params.log_blowup;
410
411            let bits_reduced = log_global_max_height - log_height;
412            let rev_reduced_index = reverse_bits_len(index >> bits_reduced, log_height);
413
414            // TODO: this can be nicer with domain methods?
415
416            // Compute gh^i
417            let x = Val::GENERATOR
418                * Val::two_adic_generator(log_height).exp_u64(rev_reduced_index as u64);
419
420            let (alpha_pow, ro) = reduced_openings
421                .entry(log_height) // Get a mutable reference to the entry.
422                .or_insert((Challenge::ONE, Challenge::ZERO));
423
424            // For each polynomial `f` in our matrix, compute `(f(z) - f(x))/(z - x)`,
425            // scale by the appropriate alpha power and add to the reduced opening for this log_height.
426            for (z, ps_at_z) in mat_points_and_values {
427                let quotient = (*z - x).inverse();
428                for (&p_at_x, &p_at_z) in zip_eq(mat_opening, ps_at_z, FriError::InvalidProofShape)?
429                {
430                    // Note we just checked batch proofs to ensure p_at_x is correct.
431                    // x, z were sent by the verifier.
432                    // ps_at_z was sent to the verifier and we are using fri to prove it is correct.
433                    *ro += *alpha_pow * (p_at_z - p_at_x) * quotient;
434                    *alpha_pow *= alpha;
435                }
436            }
437        }
438
439        // `reduced_openings` would have a log_height = log_blowup entry only if there was a
440        // trace matrix of height 1. In this case `f` is constant, so `f(zeta) - f(x))/(zeta - x)`
441        // must equal `0`.
442        if let Some((_, ro)) = reduced_openings.get(&params.log_blowup)
443            && !ro.is_zero()
444        {
445            return Err(FriError::FinalPolyMismatch);
446        }
447    }
448
449    // Return reduced openings descending by log_height.
450    Ok(reduced_openings
451        .into_iter()
452        .rev()
453        .map(|(log_height, (_, ro))| (log_height, ro))
454        .collect())
455}