p3_fri/
prover.rs

1use alloc::vec;
2use alloc::vec::Vec;
3use core::iter;
4
5use itertools::{Itertools, izip};
6use p3_challenger::{CanObserve, FieldChallenger, GrindingChallenger};
7use p3_commit::{BatchOpening, Mmcs};
8use p3_dft::{Radix2DFTSmallBatch, TwoAdicSubgroupDft};
9use p3_field::{ExtensionField, Field, TwoAdicField};
10use p3_matrix::dense::RowMajorMatrix;
11use p3_util::{log2_strict_usize, reverse_slice_index_bits};
12use tracing::{debug_span, info_span, instrument};
13
14use crate::{
15    CommitPhaseProofStep, FriFoldingStrategy, FriParameters, FriProof, ProverDataWithOpeningPoints,
16    QueryProof,
17};
18
19/// Create a proof that an opening `f(zeta)` is correct by proving that the
20/// function `(f(x) - f(zeta))/(x - zeta)` is low degree.
21///
22/// This further supports proving a batch of these claims for a collection of polynomials of shrinking degrees.
23/// Polynomials of equal degree can be combined using randomness before calling this function.
24///
25/// The Soundness error from prove_fri comes from the paper:
26/// Proximity Gaps for Reed-Solomon Codes (https://eprint.iacr.org/2020/654)
27/// and is either `rate^{num_queries}` or `rate^{num_queries/2}` depending on if you rely on conjectured or
28/// proven soundness. Particularly safety conscious users may want to set `num_queries` slightly higher than
29/// this to account for the fact that most implementations batch inputs using a single random challenge
30/// instead of one challenge for each polynomial and due to the birthday paradox,
31/// there is a non trivial chance that two queried indices will be equal.
32///
33/// Arguments:
34/// - `folding`: The FRI folding scheme to use.
35/// - `params`: The parameters for the specific FRI protocol instance.
36/// - `inputs`: The evaluation vectors of all polynomials we are applying FRI to. The function assumes that
37///   commitments to these vectors have been produced and observed by the challenger earlier in the protocol.
38/// - `challenger`: The Fiat-Shamir challenger to use for sampling challenges.
39/// - `log_global_max_height`: The log of the maximum height of the input matrices.
40/// - `prover_data_with_opening_points`: A list of pairs of a batch commitment to a collection
41///   of matrices and a list of points to open those matrices at.
42#[instrument(name = "FRI prover", skip_all)]
43pub fn prove_fri<Folding, Val, Challenge, InputMmcs, FriMmcs, Challenger>(
44    folding: &Folding,
45    params: &FriParameters<FriMmcs>,
46    inputs: Vec<Vec<Challenge>>,
47    challenger: &mut Challenger,
48    log_global_max_height: usize,
49    prover_data_with_opening_points: &[ProverDataWithOpeningPoints<
50        '_,
51        Challenge,
52        InputMmcs::ProverData<RowMajorMatrix<Val>>,
53    >],
54    input_mmcs: &InputMmcs,
55) -> FriProof<Challenge, FriMmcs, Challenger::Witness, Folding::InputProof>
56where
57    Val: TwoAdicField,
58    Challenge: ExtensionField<Val>,
59    InputMmcs: Mmcs<Val>,
60    FriMmcs: Mmcs<Challenge>,
61    Challenger: FieldChallenger<Val> + GrindingChallenger + CanObserve<FriMmcs::Commitment>,
62    Folding: FriFoldingStrategy<Val, Challenge, InputProof = Vec<BatchOpening<Val, InputMmcs>>>,
63{
64    assert!(!inputs.is_empty());
65    assert!(
66        inputs
67            .iter()
68            .tuple_windows()
69            .all(|(l, r)| l.len() >= r.len()),
70        "Inputs are not sorted in descending order of length."
71    );
72
73    let log_max_height = log2_strict_usize(inputs[0].len());
74    let log_min_height = log2_strict_usize(inputs.last().unwrap().len());
75    if params.log_final_poly_len > 0 {
76        // Final_poly_degree must be less than or equal to the degree of the smallest polynomial.
77        assert!(log_min_height > params.log_final_poly_len + params.log_blowup);
78    }
79
80    // Continually fold the inputs down until the polynomial degree reaches final_poly_degree.
81    // Returns a vector of commitments to the intermediate stage polynomials, the intermediate stage polynomials
82    // themselves and the final polynomial.
83    // Note that the challenger observes the commitments and the final polynomial inside this function so we don't
84    // need to observe the output of this function here.
85    let commit_phase_result = commit_phase(folding, params, inputs, challenger);
86
87    // Produce a proof of work witness before receiving any query challenges.
88    // This helps to prevent grinding attacks.
89    let pow_witness = challenger.grind(params.query_proof_of_work_bits);
90
91    let query_proofs = info_span!("query phase").in_scope(|| {
92        // Sample num_queries indexes to check.
93        // The probability that no two FRI indices are equal (ignoring extra query index bits) is:
94        // (Grabbed this from wikipedia page on the birthday problem)
95        // N!/(N^{num_queries} * (N - num_queries)!) ~ (1 - 1/N)^{num_queries * (num_queries - 1)/2}
96        //                                           ~ (1 - num_queries^2/2N)
97        // Here N = 2^log_max_height.
98        // With num_queries = 100, N = 2^20, this is 0.995 so there is a .5% chance of a collision.
99        // Due to this, security conscious users may want to set num_queries a little higher than the
100        // theoretical minimum.
101        iter::repeat_with(|| {
102            let index = challenger.sample_bits(log_max_height + folding.extra_query_index_bits());
103            // For each index, create a proof that the folding operations along the chain:
104            // round 0: index, round 1: index >> 1, round 2: index >> 2, ... are correct.
105            QueryProof {
106                input_proof: open_input(
107                    log_global_max_height,
108                    index,
109                    prover_data_with_opening_points,
110                    input_mmcs,
111                ),
112                commit_phase_openings: answer_query(
113                    params,
114                    &commit_phase_result.data,
115                    index >> folding.extra_query_index_bits(),
116                ),
117            }
118        })
119        .take(params.num_queries)
120        .collect()
121    });
122
123    FriProof {
124        commit_phase_commits: commit_phase_result.commits,
125        commit_pow_witnesses: commit_phase_result.pow_witnesses,
126        query_proofs,
127        final_poly: commit_phase_result.final_poly,
128        query_pow_witness: pow_witness,
129    }
130}
131
132struct CommitPhaseResult<F: Field, M: Mmcs<F>, Witness> {
133    commits: Vec<M::Commitment>,
134    data: Vec<M::ProverData<RowMajorMatrix<F>>>,
135    pow_witnesses: Vec<Witness>,
136    final_poly: Vec<F>,
137}
138
139/// Perform the commit phase of the FRI protocol.
140///
141/// In each round we reduce our evaluations over `H` to evaluations over `H^2` by defining
142/// ```text
143///     f_{i + 1}(x^2) = (f_i(x) + f_i(-x))/2 + beta_i (f_i(x) - f_i(-x))/2x
144/// ```
145/// We then commit to the evaluation vector of `f_{i + 1}` over `H^2`.
146///
147/// Once the degree of our polynomial falls below `final_poly_degree`, we compute the coefficients of our
148/// polynomial and return them along with all intermediate evaluations and corresponding commitments.
149///
150/// Arguments:
151/// - `folding`: The FRI folding scheme used by the prover.
152/// - `params`: The parameters for the specific FRI protocol instance.
153/// - `inputs`: The evaluation vectors of the polynomials. These must be sorted in descending order of length and each
154///   evaluation vector must be in bit reversed order. This function assumes that commitments to these vectors
155///   have already been produced and observed by the challenger.
156/// - `challenger`: The Fiat-Shamir challenger to use for sampling challenges.
157#[instrument(name = "commit phase", skip_all)]
158fn commit_phase<Folding, Val, Challenge, M, Challenger>(
159    folding: &Folding,
160    params: &FriParameters<M>,
161    inputs: Vec<Vec<Challenge>>,
162    challenger: &mut Challenger,
163) -> CommitPhaseResult<Challenge, M, <Challenger as GrindingChallenger>::Witness>
164where
165    Val: TwoAdicField,
166    Challenge: ExtensionField<Val>,
167    M: Mmcs<Challenge>,
168    Challenger: FieldChallenger<Val> + GrindingChallenger + CanObserve<M::Commitment>,
169    Folding: FriFoldingStrategy<Val, Challenge>,
170{
171    let mut inputs_iter = inputs.into_iter().peekable();
172    let mut folded = inputs_iter.next().unwrap();
173    let mut commits = vec![];
174    let mut data = vec![];
175    let mut pow_witnesses = vec![];
176
177    while folded.len() > params.blowup() * params.final_poly_len() {
178        // As folded is in bit reversed order, it looks like:
179        //      `[f_i(h^0), f_i(h^{N/2}), f_i(h^{N/4}), f_i(h^{3N/4}), ...] = [f_i(1), f_i(-1), f_i(h^{N/4}), f_i(-h^{N/4}), ...]`
180        // so the relevant evaluations are adjacent and we can just reinterpret the vector as a matrix of width 2.
181        let leaves = RowMajorMatrix::new(folded, 2);
182
183        // Commit to these evaluations and observe the commitment.
184        let (commit, prover_data) = params.mmcs.commit_matrix(leaves);
185        challenger.observe(commit.clone());
186        commits.push(commit);
187
188        // Produce a proof of work witness after observing the commitment and
189        // before the Fiat-Shamir batching challenge.
190        let pow_witness = challenger.grind(params.commit_proof_of_work_bits);
191        pow_witnesses.push(pow_witness);
192
193        // Get the Fiat-Shamir challenge for this round.
194        let beta: Challenge = challenger.sample_algebra_element();
195
196        // We passed ownership of `leaves` to the MMCS, so get a reference to it
197        let leaves = params.mmcs.get_matrices(&prover_data).pop().unwrap();
198        // Do the folding operation:
199        //      `f_{i + 1}'(x^2) = (f_i(x) + f_i(-x))/2 + beta_i (f_i(x) - f_i(-x))/2x`
200        folded = folding.fold_matrix(beta, leaves.as_view());
201
202        data.push(prover_data);
203
204        // If we have reached the size of the next input vector, we can add it to the current vector.
205        if let Some(v) = inputs_iter.next_if(|v| v.len() == folded.len()) {
206            // Each element of `inputs_iter` is a reduced opening polynomial, which is itself a
207            // random linear combination `f_{i, 0} + alpha f_{i, 1} + ...`, when we add it
208            // to the current folded polynomial, we need to multiply by a random factor.
209            izip!(&mut folded, v).for_each(|(c, x)| *c += beta.square() * x);
210        }
211    }
212
213    // Now we need to get the coefficients of the final polynomial. As we know that the degree
214    // is `<= params.final_poly_len()` and the evaluations are stored in bit-reversed order,
215    // we can just truncate the folded vector, bit-reverse again and run an IDFT.
216    folded.truncate(params.final_poly_len());
217    reverse_slice_index_bits(&mut folded);
218    let final_poly = debug_span!("idft final poly")
219        .in_scope(|| Radix2DFTSmallBatch::default().idft_algebra(folded));
220
221    // Observe all coefficients of the final polynomial.
222    challenger.observe_algebra_slice(&final_poly);
223
224    CommitPhaseResult {
225        commits,
226        data,
227        pow_witnesses,
228        final_poly,
229    }
230}
231
232/// Given an `index` produce a proof that the chain of folds at `index, index >> 1, ... ` are correct.
233/// This is the prover's complement to the verifier's [`verify_query`] function.
234///
235/// In addition to the output of this function, the prover must also supply the verifier with the input values
236/// (with associated opening proofs). These are produced by the `open_input` function passed into `prove_fri`.
237///
238/// For each `i` in `[0, ..., num_folds)` this returns the value at `(index >> i) ^ 1` in round `i` along with
239/// an opening proof. The verifier can then use the values in round `i` at `index >> i` and `(index >> i) ^ 1`
240/// along with possibly an input value to compute the value at `index >> (i + 1)` in round `i + 1`.
241///
242/// We repeat until we reach the final round where the verifier can check the value against the
243/// polynomial they were sent.
244///
245/// Arguments:
246/// - `params`: The parameters for the specific FRI protocol instance.
247/// - `folded_polynomial_commits`: A slice of commitments to the intermediate stage polynomials.
248/// - `start_index`: The opening index for the unfolded polynomial. For folded polynomials,
249///   we use this index right shifted by the number of folds.
250#[inline]
251fn answer_query<F, M>(
252    config: &FriParameters<M>,
253    folded_polynomial_commits: &[M::ProverData<RowMajorMatrix<F>>],
254    start_index: usize,
255) -> Vec<CommitPhaseProofStep<F, M>>
256where
257    F: Field,
258    M: Mmcs<F>,
259{
260    folded_polynomial_commits
261        .iter()
262        .enumerate()
263        .map(|(i, commit)| {
264            // After i folding rounds, the current index we are looking at is `index >> i`.
265            let index_i = start_index >> i;
266            let index_i_sibling = index_i ^ 1;
267            let index_pair = index_i >> 1;
268
269            // Get a proof that the pair of indices are correct.
270            let (mut opened_rows, opening_proof) =
271                config.mmcs.open_batch(index_pair, commit).unpack();
272
273            // opened_rows should contain just the value at index_i and its sibling.
274            // We just need to get the sibling.
275            assert_eq!(opened_rows.len(), 1);
276            let opened_row = &opened_rows.pop().unwrap();
277            assert_eq!(opened_row.len(), 2, "Committed data should be in pairs");
278            let sibling_value = opened_row[index_i_sibling % 2];
279
280            // Add the sibling and the proof to the vector.
281            CommitPhaseProofStep {
282                sibling_value,
283                opening_proof,
284            }
285        })
286        .collect()
287}
288
289/// Given an index, produce batch opening proofs for each collection of matrices
290/// combined into a single mmcs commitment.
291///
292/// In cases where the maximum height of a batch of matrices is smaller than the
293/// global max height, shift the index down to compensate.
294///
295/// Arguments:
296/// - `log_global_max_height`: The log of the maximum height of the input matrices.
297/// - `index`: The index to open the matrices at.
298/// - `prover_data_with_opening_points`: A list of pairs of a batch commitment to a collection
299///   of matrices and a list of points to open those matrices at.
300/// - `mmcs`: The mixed matrix commitment scheme used to produce the batch commitments.
301#[inline]
302fn open_input<Val, Challenge, InputMmcs>(
303    log_global_max_height: usize,
304    index: usize,
305    prover_data_with_opening_points: &[ProverDataWithOpeningPoints<
306        '_,
307        Challenge,
308        InputMmcs::ProverData<RowMajorMatrix<Val>>,
309    >],
310    mmcs: &InputMmcs,
311) -> Vec<BatchOpening<Val, InputMmcs>>
312where
313    Val: TwoAdicField,
314    Challenge: ExtensionField<Val>,
315    InputMmcs: Mmcs<Val>,
316{
317    // This gives the verifier access to evaluations `f(x)` from which it can compute
318    // `(f(zeta) - f(x))/(zeta - x)` and then combine them together and roll into FRI
319    // as appropriate.
320    prover_data_with_opening_points
321        .iter()
322        .map(|(data, _)| {
323            let log_max_height = log2_strict_usize(mmcs.get_max_height(data));
324            let bits_reduced = log_global_max_height - log_max_height;
325            // If a matrix is smaller than global max height, we roll it into
326            // fri in a later round.
327            let reduced_index = index >> bits_reduced;
328            mmcs.open_batch(reduced_index, data)
329        })
330        .collect()
331}