Skip to main content

slop_basefold/
verifier.rs

1use itertools::Itertools;
2use serde::{Deserialize, Serialize};
3use slop_algebra::{AbstractExtensionField, AbstractField, TwoAdicField};
4use slop_challenger::{
5    CanObserve, CanSampleBits, FieldChallenger, GrindingChallenger, IopCtx,
6    VariableLengthChallenger,
7};
8use slop_merkle_tree::{MerkleTreeOpeningAndProof, MerkleTreeTcs, MerkleTreeTcsError};
9use slop_multilinear::{partial_lagrange_blocking, MleEval, MultilinearPcsChallenger, Point};
10use slop_utils::reverse_bits_len;
11use thiserror::Error;
12
13pub use slop_primitives::FriConfig;
14
15/// The number of bits to grind in sampling the batching randomness.
16pub const BATCH_GRINDING_BITS: usize = 5;
17
18#[derive(Clone)]
19pub struct BasefoldVerifier<GC: IopCtx> {
20    pub fri_config: crate::FriConfig<GC::F>,
21    pub tcs: MerkleTreeTcs<GC>,
22    pub num_expected_commitments: usize,
23}
24
25impl<GC: IopCtx> std::fmt::Debug for BasefoldVerifier<GC> {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("BasefoldVerifier")
28            .field("fri_config", &self.fri_config)
29            .field("num_expected_commitments", &self.num_expected_commitments)
30            .finish()
31    }
32}
33
34impl<GC: IopCtx> BasefoldVerifier<GC> {
35    pub fn new(fri_config: crate::FriConfig<GC::F>, num_expected_commitments: usize) -> Self {
36        assert_ne!(num_expected_commitments, 0, "commitment must exist");
37        Self { fri_config, tcs: MerkleTreeTcs::default(), num_expected_commitments }
38    }
39}
40
41#[derive(Error)]
42pub enum BaseFoldVerifierError<TcsError> {
43    #[error("Sumcheck and FRI commitments length mismatch")]
44    SumcheckFriLengthMismatch,
45    #[error("Query failed to verify: {0}")]
46    TcsError(#[from] TcsError),
47    #[error("Sumcheck error")]
48    Sumcheck,
49    #[error("Invalid proof of work witness")]
50    Pow,
51    #[error("Invalid batch grinding witness")]
52    BatchPow,
53    #[error("Query value mismatch")]
54    QueryValueMismatch,
55    #[error("query final polynomial mismatch")]
56    QueryFinalPolyMismatch,
57    #[error("sumcheck final polynomial mismatch")]
58    SumcheckFinalPolyMismatch,
59    #[error("incorrect shape of proof")]
60    IncorrectShape,
61    #[error("instance overflows the field two_adicity")]
62    TwoAdicityOverflow,
63}
64
65impl<TcsError: std::fmt::Display> std::fmt::Debug for BaseFoldVerifierError<TcsError> {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            BaseFoldVerifierError::SumcheckFriLengthMismatch => {
69                write!(f, "sumcheck and FRI commitments length mismatch")
70            }
71            BaseFoldVerifierError::TcsError(e) => write!(f, "tensor opening error: {e}"),
72            BaseFoldVerifierError::Sumcheck => write!(f, "sumcheck error"),
73            BaseFoldVerifierError::Pow => write!(f, "invalid proof of work witness"),
74            BaseFoldVerifierError::BatchPow => {
75                write!(f, "invalid batch grinding witness")
76            }
77            BaseFoldVerifierError::QueryValueMismatch => write!(f, "query value mismatch"),
78            BaseFoldVerifierError::QueryFinalPolyMismatch => {
79                write!(f, "query final polynomial mismatch")
80            }
81            BaseFoldVerifierError::SumcheckFinalPolyMismatch => {
82                write!(f, "sumcheck final polynomial mismatch")
83            }
84            BaseFoldVerifierError::IncorrectShape => {
85                write!(f, "incorrect shape of proof")
86            }
87            BaseFoldVerifierError::TwoAdicityOverflow => {
88                write!(f, "instance overflows the field two_adicity")
89            }
90        }
91    }
92}
93
94/// A proof of a Basefold evaluation claim.
95#[derive(Clone, Serialize, Deserialize)]
96#[serde(bound(serialize = "", deserialize = ""))]
97pub struct BasefoldProof<GC: IopCtx> {
98    /// The univariate polynomials that are used in the sumcheck part of the BaseFold protocol.
99    pub univariate_messages: Vec<[GC::EF; 2]>,
100    /// The FRI parts of the proof.
101    /// The commitments to the folded polynomials produced in the commit phase.
102    pub fri_commitments: Vec<GC::Digest>,
103    /// The query openings for the individual multilinear polynmomials.
104    ///
105    /// The vector is indexed by the batch number.
106    pub component_polynomials_query_openings_and_proofs: Vec<MerkleTreeOpeningAndProof<GC>>,
107    /// The query openings and the FRI query proofs for the FRI query phase.
108    pub query_phase_openings_and_proofs: Vec<MerkleTreeOpeningAndProof<GC>>,
109    /// The prover performs FRI until we reach a polynomial of degree 0, and return the constant
110    /// value of this polynomial.
111    pub final_poly: GC::EF,
112    /// Proof-of-work witness.
113    pub pow_witness: <GC::Challenger as GrindingChallenger>::Witness,
114    /// Batch grinding witness.
115    pub batch_grinding_witness: <GC::Challenger as GrindingChallenger>::Witness,
116}
117
118impl<GC: IopCtx> BasefoldVerifier<GC>
119where
120    GC::F: TwoAdicField,
121{
122    pub fn verify_mle_evaluations(
123        &self,
124        commitments: &[GC::Digest],
125        mut point: Point<GC::EF>,
126        evaluation_claims: &[MleEval<GC::EF>],
127        proof: &BasefoldProof<GC>,
128        challenger: &mut GC::Challenger,
129    ) -> Result<(), BaseFoldVerifierError<MerkleTreeTcsError>> {
130        // Check batch grinding witness.
131        if !challenger.check_witness(BATCH_GRINDING_BITS, proof.batch_grinding_witness) {
132            return Err(BaseFoldVerifierError::BatchPow);
133        }
134
135        // Sample the challenge used to batch all the different polynomials.
136        let total_len = evaluation_claims
137            .iter()
138            .map(|batch_claims| batch_claims.num_polynomials())
139            .sum::<usize>();
140
141        let num_batching_variables = total_len.next_power_of_two().ilog2();
142        let batching_point = challenger.sample_point::<GC::EF>(num_batching_variables);
143        let batching_coefficients = partial_lagrange_blocking(&batching_point);
144
145        // Compute the batched evaluation claim.
146        let eval_claim = evaluation_claims
147            .iter()
148            .flat_map(|batch_claims| batch_claims.iter())
149            .zip(batching_coefficients.as_slice())
150            .map(|(eval, batch_power)| *eval * *batch_power)
151            .sum::<GC::EF>();
152
153        if evaluation_claims.len() != commitments.len()
154            || commitments.len() != proof.component_polynomials_query_openings_and_proofs.len()
155            || commitments.len() != self.num_expected_commitments
156        {
157            return Err(BaseFoldVerifierError::IncorrectShape);
158        }
159
160        // Assert correctness of shape.
161        if proof.fri_commitments.len() != proof.univariate_messages.len()
162            || proof.fri_commitments.len() != point.dimension()
163            || proof.univariate_messages.is_empty()
164        {
165            return Err(BaseFoldVerifierError::SumcheckFriLengthMismatch);
166        }
167
168        // The prover messages correspond to fixing the last coordinate first, so we reverse the
169        // underlying point for the verification.
170        point.reverse();
171
172        // Sample the challenges used for FRI folding and BaseFold random linear combinations.
173        // Observe the number of FRI rounds. In principle, the prover should already be
174        // bound to this length because it is deducible from the shape of the openings in
175        // `proof.component_polynomials_query_openings_and_proofs` and the prover is bound to those,
176        // but we observe it here for security.
177        let len = proof.fri_commitments.len();
178        challenger.observe(GC::F::from_canonical_usize(len));
179        let betas = proof
180            .fri_commitments
181            .iter()
182            .zip_eq(proof.univariate_messages.iter())
183            .map(|(commitment, poly)| {
184                challenger.observe_constant_length_extension_slice(poly);
185                challenger.observe(*commitment);
186                challenger.sample_ext_element::<GC::EF>()
187            })
188            .collect::<Vec<_>>();
189
190        // Check the consistency of the first univariate message with the claimed evaluation. The
191        // first_poly is supposed to be `vals(X_0, X_1, ..., X_{d-1}, 0), vals(X_0, X_1, ...,
192        // X_{d-1}, 1)`. Given this, the claimed evaluation should be `(1 - X_d) *
193        // first_poly[0] + X_d * first_poly[1]`.
194        let first_poly = proof.univariate_messages[0];
195        if eval_claim != (GC::EF::one() - *point[0]) * first_poly[0] + *point[0] * first_poly[1] {
196            println!("failed in first_poly");
197            return Err(BaseFoldVerifierError::Sumcheck);
198        };
199
200        // Fold the two messages into a single evaluation claim for the next round, using the
201        // sampled randomness.
202        let mut expected_eval = first_poly[0] + betas[0] * first_poly[1];
203
204        // Check round-by-round consistency between the successive sumcheck univariate messages.
205        for (i, (poly, beta)) in
206            proof.univariate_messages[1..].iter().zip_eq(betas[1..].iter()).enumerate()
207        {
208            // The check is similar to the one for `first_poly`.
209            let i = i + 1;
210            if expected_eval != (GC::EF::one() - *point[i]) * poly[0] + *point[i] * poly[1] {
211                println!("failed in round {i}");
212                return Err(BaseFoldVerifierError::Sumcheck);
213            }
214
215            // Fold the two pieces of the message.
216            expected_eval = poly[0] + *beta * poly[1];
217        }
218
219        challenger.observe_ext_element(proof.final_poly);
220
221        // Check proof of work (grinding to find a number that hashes to have
222        // `self.config.proof_of_work_bits` zeroes at the beginning).
223        if !challenger.check_witness(self.fri_config.proof_of_work_bits, proof.pow_witness) {
224            return Err(BaseFoldVerifierError::Pow);
225        }
226
227        let log_len = proof.fri_commitments.len();
228
229        if log_len + self.fri_config.log_blowup() > GC::F::TWO_ADICITY {
230            return Err(BaseFoldVerifierError::TwoAdicityOverflow);
231        }
232
233        // Sample query indices for the FRI query IOPP part of BaseFold. This part is very similar
234        // to the corresponding part in the univariate FRI verifier.
235        let query_indices = (0..self.fri_config.num_queries)
236            .map(|_| challenger.sample_bits(log_len + self.fri_config.log_blowup()))
237            .collect::<Vec<_>>();
238
239        // Compute the batch evaluations from the openings of the component polynomials.
240        let mut batch_evals = vec![GC::EF::zero(); query_indices.len()];
241        let mut batch_idx = 0;
242        for (round_idx, opening_and_proof) in
243            proof.component_polynomials_query_openings_and_proofs.iter().enumerate()
244        {
245            let values = &opening_and_proof.values;
246            let total_columns = evaluation_claims[round_idx].num_polynomials();
247            if values.dimensions.sizes().len() != 2 {
248                return Err(BaseFoldVerifierError::IncorrectShape);
249            }
250            if values.dimensions.sizes()[0] != query_indices.len() {
251                return Err(BaseFoldVerifierError::IncorrectShape);
252            }
253            if values.dimensions.sizes()[1] != total_columns {
254                return Err(BaseFoldVerifierError::IncorrectShape);
255            }
256            let round_coefficients =
257                &batching_coefficients.as_slice()[batch_idx..batch_idx + total_columns];
258            for (batch_eval, values) in batch_evals.iter_mut().zip_eq(values.split()) {
259                for (value, batching_coefficient) in
260                    values.as_slice().iter().zip(round_coefficients)
261                {
262                    *batch_eval += *batching_coefficient * *value;
263                }
264            }
265            batch_idx += total_columns;
266        }
267
268        // Verify the proof of the claimed values of the original commitments at the query indices.
269        for (commit, opening_and_proof) in
270            commitments.iter().zip_eq(proof.component_polynomials_query_openings_and_proofs.iter())
271        {
272            // Sizes is checked to have at least two dimensions above.
273            let width = opening_and_proof.values.sizes()[1];
274            self.tcs
275                .verify_tensor_openings(
276                    commit,
277                    &query_indices,
278                    &opening_and_proof.values,
279                    width,
280                    log_len + self.fri_config.log_blowup(),
281                    &opening_and_proof.proof,
282                )
283                .map_err(BaseFoldVerifierError::TcsError)?;
284        }
285
286        // Check that the query openings are consistent as FRI messages.
287        self.verify_queries(
288            &proof.fri_commitments,
289            &query_indices,
290            proof.final_poly,
291            batch_evals,
292            &proof.query_phase_openings_and_proofs,
293            &betas,
294        )?;
295
296        // The final consistency check between the FRI messages and the partial evaluation messages.
297        if proof.final_poly
298            != proof.univariate_messages.last().unwrap()[0]
299                + *betas.last().unwrap() * proof.univariate_messages.last().unwrap()[1]
300        {
301            return Err(BaseFoldVerifierError::SumcheckFinalPolyMismatch);
302        }
303
304        Ok(())
305    }
306
307    /// The FRI verifier for a single query. We modify this from Plonky3 to be compatible with
308    /// opening only a single vector.
309    fn verify_queries(
310        &self,
311        commitments: &[GC::Digest],
312        indices: &[usize],
313        final_poly: GC::EF,
314        reduced_openings: Vec<GC::EF>,
315        query_openings: &[MerkleTreeOpeningAndProof<GC>],
316        betas: &[GC::EF],
317    ) -> Result<(), BaseFoldVerifierError<MerkleTreeTcsError>> {
318        let log_max_height = commitments.len() + self.fri_config.log_blowup();
319
320        let mut folded_evals = reduced_openings;
321        let mut indices = indices.to_vec();
322
323        let mut xis = indices
324            .iter()
325            .map(|index| {
326                GC::F::two_adic_generator(log_max_height)
327                    .exp_u64(reverse_bits_len(*index, log_max_height) as u64)
328            })
329            .collect::<Vec<_>>();
330
331        if commitments.len() != query_openings.len() || commitments.len() != betas.len() {
332            return Err(BaseFoldVerifierError::IncorrectShape);
333        }
334
335        // Loop over the FRI queries.
336        for (round_idx, ((commitment, query_opening), beta)) in (self.fri_config.log_blowup()
337            ..log_max_height)
338            .rev()
339            .zip_eq(commitments.iter().zip_eq(query_openings.iter()).zip_eq(betas))
340        {
341            let openings = &query_opening.values;
342            if openings.dimensions.sizes().len() != 2 {
343                return Err(BaseFoldVerifierError::IncorrectShape);
344            }
345
346            if indices.len() != folded_evals.len()
347                || indices.len() != openings.dimensions.sizes()[0]
348                || indices.len() != xis.len()
349            {
350                return Err(BaseFoldVerifierError::IncorrectShape);
351            }
352
353            for (((index, folded_eval), opening), x) in indices
354                .iter_mut()
355                .zip_eq(folded_evals.iter_mut())
356                .zip_eq(openings.split())
357                .zip_eq(xis.iter_mut())
358            {
359                let index_sibling = *index ^ 1;
360                let index_pair = *index >> 1;
361
362                if opening.total_len() != 2 * <GC::EF as AbstractExtensionField<GC::F>>::D {
363                    return Err(BaseFoldVerifierError::IncorrectShape);
364                }
365
366                let evals: [GC::EF; 2] = opening
367                    .as_slice()
368                    .chunks_exact(GC::EF::D)
369                    .map(GC::EF::from_base_slice)
370                    .collect::<Vec<_>>()
371                    .try_into()
372                    .unwrap();
373
374                // Check that the folded evaluation is consistent with the FRI query proof opening.
375                if evals[*index % 2] != *folded_eval {
376                    return Err(BaseFoldVerifierError::QueryValueMismatch);
377                }
378
379                let mut xs = [*x; 2];
380                xs[index_sibling % 2] *= GC::F::two_adic_generator(1);
381
382                // interpolate and evaluate at beta
383                *folded_eval = evals[0]
384                    + (*beta - xs[0]) * (evals[1] - evals[0]) / GC::EF::from(xs[1] - xs[0]);
385
386                *index = index_pair;
387                *x = x.square();
388            }
389
390            // Check that the opening is consistent with the commitment.
391            // The magic constant 2 here is the folding factor we use for FRI.
392            self.tcs
393                .verify_tensor_openings(
394                    commitment,
395                    &indices,
396                    &query_opening.values,
397                    GC::EF::D * 2,
398                    round_idx,
399                    &query_opening.proof,
400                )
401                .map_err(BaseFoldVerifierError::TcsError)?;
402        }
403
404        for folded_eval in folded_evals {
405            if folded_eval != final_poly {
406                return Err(BaseFoldVerifierError::QueryFinalPolyMismatch);
407            }
408        }
409
410        Ok(())
411    }
412
413    pub fn verify_untrusted_evaluations(
414        &self,
415        commitments: &[GC::Digest],
416        eval_point: Point<GC::EF>,
417        evaluation_claims: &[MleEval<GC::EF>],
418        proof: &BasefoldProof<GC>,
419        challenger: &mut GC::Challenger,
420    ) -> Result<(), BaseFoldVerifierError<MerkleTreeTcsError>> {
421        // Observe the evaluation claims.
422        for round in evaluation_claims.iter() {
423            // We assume that in the process of producing `commitments`, the prover is bound
424            // to the number of polynomials in each round. Thus, we can observe the evaluation
425            // claims without observing their length.
426            challenger.observe_constant_length_extension_slice(round);
427        }
428
429        self.verify_mle_evaluations(commitments, eval_point, evaluation_claims, proof, challenger)
430    }
431}