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