Skip to main content

poly_commitment/
kzg.rs

1//! KZG polynomial commitment scheme (KZG10).
2//!
3//! This module implements the KZG protocol described in the paper
4//! [Constant-Size Commitments to Polynomials and Their
5//! Applications](https://www.iacr.org/archive/asiacrypt2010/6477178/6477178.pdf)
6//! by Kate, Zaverucha and Goldberg, often referred to as the KZG10 paper.
7//!
8//! The protocol requires a structured reference string (SRS) that contains
9//! powers of a generator of a group, and a pairing friendly curve.
10//!
11//! The pairing friendly curve requirement is hidden in the Pairing trait
12//! parameter.
13
14use crate::{
15    commitment::{
16        combine_commitments, BatchEvaluationProof, BlindedCommitment, CommitmentCurve, Evaluation,
17        PolyComm,
18    },
19    error::CommitmentError,
20    ipa::SRS,
21    utils::combine_polys,
22    PolynomialsToCombine, SRS as SRSTrait,
23};
24use alloc::{vec, vec::Vec};
25
26use ark_ec::{pairing::Pairing, AffineRepr, VariableBaseMSM};
27use ark_ff::{One, PrimeField, Zero};
28use ark_poly::{
29    univariate::{DenseOrSparsePolynomial, DensePolynomial},
30    DenseUVPolynomial, EvaluationDomain, Evaluations, Polynomial, Radix2EvaluationDomain as D,
31};
32use core::ops::Neg;
33use mina_poseidon::FqSponge;
34#[cfg(feature = "std")]
35use rand::thread_rng;
36use rand_core::{CryptoRng, RngCore};
37use serde::{Deserialize, Serialize};
38use serde_with::serde_as;
39#[cfg(feature = "std")]
40use std::ops::Deref;
41
42/// Combine the (chunked) evaluations of multiple polynomials.
43///
44/// This function returns the accumulation of the evaluations, scaled by
45/// `polyscale`.
46/// If no evaluation is given, the function returns an empty vector.
47/// It does also suppose that for each evaluation, the number of evaluations is
48/// the same. It is not constrained yet in the interface, but it should be. If
49/// one list has not the same size, it will be shrunk to the size of the first
50/// element of the list.
51/// For instance, if we have 3 polynomials P1, P2, P3 evaluated at the points
52/// ζ and ζω (like in vanilla `PlonK`), and for each polynomial, we have two
53/// chunks, i.e. we have
54/// ```text
55///         2 chunks of P1
56///        /---------------\
57/// E1 = [(P1_1(ζ), P1_2(ζ)), (P1_1(ζω), P1_2(ζω))]
58/// E2 = [(P2_1(ζ), P2_2(ζ)), (P2_1(ζω), P2_2(ζω))]
59/// E3 = [(P3_1(ζ), P3_2(ζ)), (P3_1(ζω), P3_2(ζω))]
60/// ```
61/// The output will be a list of 3 elements, equal to:
62/// ```text
63/// P1_1(ζ) + P1_2(ζ) * polyscale + P1_1(ζω) polyscale^2 + P1_2(ζω) * polyscale^3
64/// P2_1(ζ) + P2_2(ζ) * polyscale + P2_1(ζω) polyscale^2 + P2_2(ζω) * polyscale^3
65/// ```
66pub fn combine_evaluations<G: CommitmentCurve>(
67    evaluations: &[Evaluation<G>],
68    polyscale: G::ScalarField,
69) -> Vec<G::ScalarField> {
70    let mut polyscale_i = G::ScalarField::one();
71    let mut acc = {
72        let num_evals = evaluations.first().map_or(0, |e| e.evaluations.len());
73        vec![G::ScalarField::zero(); num_evals]
74    };
75
76    for Evaluation { evaluations, .. } in evaluations.iter().filter(|x| !x.commitment.is_empty()) {
77        // IMPROVEME: we could have a flat array that would contain all the
78        // evaluations and all the chunks. It would avoid fetching the memory
79        // and avoid indirection into RAM.
80        // We could have a single flat array.
81        // iterating over the polynomial segments
82        for chunk_idx in 0..evaluations[0].len() {
83            // supposes that all evaluations are of the same size
84            for eval_pt_idx in 0..evaluations.len() {
85                acc[eval_pt_idx] += evaluations[eval_pt_idx][chunk_idx] * polyscale_i;
86            }
87            polyscale_i *= polyscale;
88        }
89    }
90
91    acc
92}
93
94#[serde_as]
95#[derive(Debug, Serialize, Deserialize)]
96#[serde(
97    bound = "Pair::G1Affine: ark_serialize::CanonicalDeserialize + ark_serialize::CanonicalSerialize"
98)]
99pub struct KZGProof<Pair: Pairing> {
100    #[serde_as(as = "o1_utils::serialization::SerdeAs")]
101    pub quotient: Pair::G1Affine,
102    #[serde_as(as = "o1_utils::serialization::SerdeAs")]
103    /// A blinding factor used to hide the polynomial, if necessary
104    pub blinding: <Pair::G1Affine as AffineRepr>::ScalarField,
105}
106
107impl<Pair: Pairing> Default for KZGProof<Pair> {
108    fn default() -> Self {
109        Self {
110            quotient: Pair::G1Affine::generator(),
111            blinding: <Pair::G1Affine as AffineRepr>::ScalarField::zero(),
112        }
113    }
114}
115
116impl<Pair: Pairing> Clone for KZGProof<Pair> {
117    fn clone(&self) -> Self {
118        Self {
119            quotient: self.quotient,
120            blinding: self.blinding,
121        }
122    }
123}
124
125/// Define a structured reference string for the KZG protocol.
126///
127/// The SRS (i.e. [`PairingSRS`]) consists of powers of an element `g^x`
128/// for some toxic waste `x`.
129///
130/// The SRS is formed using what we call a "trusted setup". For now, the setup
131/// is created using the method `create_trusted_setup_with_toxic_waste`.
132#[derive(Debug, PartialEq, Serialize, Deserialize)]
133pub struct PairingSRS<Pair: Pairing> {
134    /// The full SRS is the one used by the prover. Can be seen as the "proving
135    /// key"/"secret key"
136    pub full_srs: SRS<Pair::G1Affine>,
137    /// SRS to be used by the verifier. Can be seen as the "verification
138    /// key"/"public key".
139    pub verifier_srs: SRS<Pair::G2Affine>,
140}
141
142#[cfg(feature = "std")]
143impl<
144        F: PrimeField,
145        G: CommitmentCurve<ScalarField = F>,
146        G2: CommitmentCurve<ScalarField = F>,
147        Pair: Pairing<G1Affine = G, G2Affine = G2>,
148    > PairingSRS<Pair>
149{
150    /// Create a trusted setup for the KZG protocol.
151    ///
152    /// # Security
153    ///
154    /// The caller must ensure that `toxic_waste` is securely zeroized
155    /// after use. Leaking it compromises the soundness of the proof system.
156    pub fn create_trusted_setup_with_toxic_waste(toxic_waste: F, depth: usize) -> Self {
157        let full_srs = SRS::create_trusted_setup_with_toxic_waste(toxic_waste, depth);
158        let verifier_srs = SRS::create_trusted_setup_with_toxic_waste(toxic_waste, 3);
159        Self {
160            full_srs,
161            verifier_srs,
162        }
163    }
164}
165
166impl<Pair: Pairing> Default for PairingSRS<Pair> {
167    fn default() -> Self {
168        Self {
169            full_srs: SRS::default(),
170            verifier_srs: SRS::default(),
171        }
172    }
173}
174
175impl<Pair: Pairing> Clone for PairingSRS<Pair> {
176    fn clone(&self) -> Self {
177        Self {
178            full_srs: self.full_srs.clone(),
179            verifier_srs: self.verifier_srs.clone(),
180        }
181    }
182}
183
184#[cfg(feature = "std")]
185impl<
186        F: PrimeField,
187        G: CommitmentCurve<ScalarField = F>,
188        G2: CommitmentCurve<ScalarField = F>,
189        Pair: Pairing<G1Affine = G, G2Affine = G2>,
190        const FULL_ROUNDS: usize,
191    > crate::OpenProof<G, FULL_ROUNDS> for KZGProof<Pair>
192{
193    type SRS = PairingSRS<Pair>;
194
195    /// Parameters:
196    /// - `srs`: the structured reference string
197    /// - `plnms`: vector of polynomials with optional degree bound and
198    ///   commitment randomness
199    /// - `elm`: vector of evaluation points
200    /// - `polyscale`: scaling factor for polynoms
201    ///   `group_maps`, `sponge`, `rng` and `evalscale` are not used. The
202    ///   parameters are kept to fit the trait and to be used generically.
203    fn open<EFqSponge, RNG, D: EvaluationDomain<F>>(
204        srs: &Self::SRS,
205        _group_map: &<G as CommitmentCurve>::Map,
206        plnms: PolynomialsToCombine<G, D>,
207        elm: &[<G as AffineRepr>::ScalarField],
208        polyscale: <G as AffineRepr>::ScalarField,
209        _evalscale: <G as AffineRepr>::ScalarField,
210        _sponge: EFqSponge,
211        _rng: &mut RNG,
212    ) -> Self
213    where
214        EFqSponge: Clone + FqSponge<<G as AffineRepr>::BaseField, G, F, FULL_ROUNDS>,
215        RNG: RngCore + CryptoRng,
216    {
217        Self::create(srs, plnms, elm, polyscale).unwrap()
218    }
219
220    fn verify<EFqSponge, RNG>(
221        srs: &Self::SRS,
222        _group_map: &G::Map,
223        batch: &mut [BatchEvaluationProof<G, EFqSponge, Self, FULL_ROUNDS>],
224        _rng: &mut RNG,
225    ) -> bool
226    where
227        EFqSponge: FqSponge<G::BaseField, G, F, FULL_ROUNDS>,
228        RNG: RngCore + CryptoRng,
229    {
230        for BatchEvaluationProof {
231            sponge: _,
232            evaluations,
233            evaluation_points,
234            polyscale,
235            evalscale: _,
236            opening,
237            combined_inner_product: _,
238        } in batch.iter()
239        {
240            if !opening.verify(srs, evaluations, *polyscale, evaluation_points) {
241                return false;
242            }
243        }
244        true
245    }
246}
247
248#[cfg(feature = "std")]
249impl<
250        F: PrimeField,
251        G: CommitmentCurve<ScalarField = F>,
252        G2: CommitmentCurve<ScalarField = F>,
253        Pair: Pairing<G1Affine = G, G2Affine = G2>,
254    > SRSTrait<G> for PairingSRS<Pair>
255{
256    fn max_poly_size(&self) -> usize {
257        self.full_srs.max_poly_size()
258    }
259
260    fn get_lagrange_basis(
261        &self,
262        domain: D<G::ScalarField>,
263    ) -> impl Deref<Target = Vec<PolyComm<G>>> + '_ {
264        self.full_srs.get_lagrange_basis(domain)
265    }
266
267    fn get_lagrange_basis_from_domain_size(
268        &self,
269        domain_size: usize,
270    ) -> impl Deref<Target = Vec<PolyComm<G>>> + '_ {
271        self.full_srs
272            .get_lagrange_basis_from_domain_size(domain_size)
273    }
274
275    fn blinding_commitment(&self) -> G {
276        self.full_srs.blinding_commitment()
277    }
278
279    fn mask_custom(
280        &self,
281        com: PolyComm<G>,
282        blinders: &PolyComm<G::ScalarField>,
283    ) -> Result<BlindedCommitment<G>, CommitmentError> {
284        self.full_srs.mask_custom(com, blinders)
285    }
286
287    fn mask(
288        &self,
289        comm: PolyComm<G>,
290        rng: &mut (impl RngCore + CryptoRng),
291    ) -> BlindedCommitment<G> {
292        self.full_srs.mask(comm, rng)
293    }
294
295    fn commit(
296        &self,
297        plnm: &DensePolynomial<F>,
298        num_chunks: usize,
299        rng: &mut (impl RngCore + CryptoRng),
300    ) -> BlindedCommitment<G> {
301        self.full_srs.commit(plnm, num_chunks, rng)
302    }
303
304    fn commit_non_hiding(
305        &self,
306        plnm: &DensePolynomial<G::ScalarField>,
307        num_chunks: usize,
308    ) -> PolyComm<G> {
309        self.full_srs.commit_non_hiding(plnm, num_chunks)
310    }
311
312    fn commit_custom(
313        &self,
314        plnm: &DensePolynomial<<G>::ScalarField>,
315        num_chunks: usize,
316        blinders: &PolyComm<<G>::ScalarField>,
317    ) -> Result<BlindedCommitment<G>, CommitmentError> {
318        self.full_srs.commit_custom(plnm, num_chunks, blinders)
319    }
320
321    fn commit_evaluations_non_hiding(
322        &self,
323        domain: D<G::ScalarField>,
324        plnm: &Evaluations<G::ScalarField, D<G::ScalarField>>,
325    ) -> PolyComm<G> {
326        self.full_srs.commit_evaluations_non_hiding(domain, plnm)
327    }
328
329    fn commit_evaluations(
330        &self,
331        domain: D<G::ScalarField>,
332        plnm: &Evaluations<G::ScalarField, D<G::ScalarField>>,
333        rng: &mut (impl RngCore + CryptoRng),
334    ) -> BlindedCommitment<G> {
335        self.full_srs.commit_evaluations(domain, plnm, rng)
336    }
337
338    fn commit_evaluations_custom(
339        &self,
340        domain: D<<G>::ScalarField>,
341        plnm: &Evaluations<<G>::ScalarField, D<<G>::ScalarField>>,
342        blinders: &PolyComm<<G>::ScalarField>,
343    ) -> Result<BlindedCommitment<G>, CommitmentError> {
344        self.full_srs
345            .commit_evaluations_custom(domain, plnm, blinders)
346    }
347
348    fn create(depth: usize) -> Self {
349        let mut rng = thread_rng();
350        let mut toxic_waste = G::ScalarField::rand(&mut rng);
351        let result = Self::create_trusted_setup_with_toxic_waste(toxic_waste, depth);
352        toxic_waste.zeroize();
353        result
354    }
355
356    fn size(&self) -> usize {
357        self.full_srs.g.len()
358    }
359}
360
361/// The polynomial that evaluates to each of `evals` for the respective `elm`s.
362/// For now, only works for 2 evaluations points.
363/// `elm` is the vector of evaluation points and `evals` is the vector of
364/// evaluations at those points.
365fn eval_polynomial<F: PrimeField>(elm: &[F], evals: &[F]) -> DensePolynomial<F> {
366    assert_eq!(elm.len(), evals.len());
367    let (zeta, zeta_omega) = if elm.len() == 2 {
368        (elm[0], elm[1])
369    } else {
370        todo!()
371    };
372    let (eval_zeta, eval_zeta_omega) = if evals.len() == 2 {
373        (evals[0], evals[1])
374    } else {
375        todo!()
376    };
377
378    // The polynomial that evaluates to `p(ζ)` at `ζ` and `p(ζω)` at
379    // `ζω`.
380    // We write `p(x) = a + bx`, which gives
381    // ```text
382    // p(ζ) = a + b * ζ
383    // p(ζω) = a + b * ζω
384    // ```
385    // and so
386    // ```text
387    // b = (p(ζω) - p(ζ)) / (ζω - ζ)
388    // a = p(ζ) - b * ζ
389    // ```
390    let b = (eval_zeta_omega - eval_zeta) / (zeta_omega - zeta);
391    let a = eval_zeta - b * zeta;
392    DensePolynomial::from_coefficients_slice(&[a, b])
393}
394
395/// The polynomial that evaluates to `0` at the evaluation points.
396fn divisor_polynomial<F: PrimeField>(elm: &[F]) -> DensePolynomial<F> {
397    elm.iter()
398        .map(|value| DensePolynomial::from_coefficients_slice(&[-(*value), F::one()]))
399        .reduce(|poly1, poly2| &poly1 * &poly2)
400        .unwrap()
401}
402
403#[cfg(feature = "std")]
404impl<
405        F: PrimeField,
406        G: CommitmentCurve<ScalarField = F>,
407        G2: CommitmentCurve<ScalarField = F>,
408        Pair: Pairing<G1Affine = G, G2Affine = G2>,
409    > KZGProof<Pair>
410{
411    /// Create a KZG proof.
412    /// Parameters:
413    /// - `srs`: the structured reference string used to commit
414    ///   to the polynomials
415    /// - `plnms`: the list of polynomials to open.
416    ///   The type is simply an alias to handle the polynomials in evaluations
417    ///   or coefficients forms.
418    /// - `elm`: vector of evaluation points. Note that it only works for two
419    ///   elements for now.
420    /// - `polyscale`: a challenge to batch the polynomials.
421    pub fn create<D: EvaluationDomain<F>>(
422        srs: &PairingSRS<Pair>,
423        plnms: PolynomialsToCombine<G, D>,
424        elm: &[F],
425        polyscale: F,
426    ) -> Option<Self> {
427        let (p, blinding_factor) = combine_polys::<G, D>(plnms, polyscale, srs.full_srs.g.len());
428        let evals: Vec<_> = elm.iter().map(|pt| p.evaluate(pt)).collect();
429
430        let quotient_poly = {
431            // This is where the condition on two points is enforced.
432            let eval_polynomial = eval_polynomial(elm, &evals);
433            let divisor_polynomial = divisor_polynomial(elm);
434            let numerator_polynomial = &p - &eval_polynomial;
435            let (quotient, remainder) = DenseOrSparsePolynomial::divide_with_q_and_r(
436                &numerator_polynomial.into(),
437                &divisor_polynomial.into(),
438            )?;
439            if !remainder.is_zero() {
440                return None;
441            }
442            quotient
443        };
444
445        let quotient = srs
446            .full_srs
447            .commit_non_hiding(&quotient_poly, 1)
448            .get_first_chunk();
449
450        Some(Self {
451            quotient,
452            blinding: blinding_factor,
453        })
454    }
455
456    /// Verify a proof. Note that it only works for two elements for now, i.e.
457    /// elm must be of size 2.
458    /// Also, chunking is not supported.
459    pub fn verify(
460        &self,
461        srs: &PairingSRS<Pair>,
462        evaluations: &[Evaluation<G>], // commitments to the polynomials
463        polyscale: F,                  // scaling factor for polynoms
464        elm: &[F],                     // vector of evaluation points
465    ) -> bool {
466        let poly_commitment: G::Group = {
467            let mut scalars: Vec<F> = Vec::new();
468            let mut points = Vec::new();
469            combine_commitments(
470                evaluations,
471                &mut scalars,
472                &mut points,
473                polyscale,
474                F::one(), /* TODO: This is inefficient */
475            );
476            let scalars: Vec<_> = scalars.iter().map(|x| x.into_bigint()).collect();
477
478            G::Group::msm_bigint(&points, &scalars)
479        };
480
481        // IMPROVEME: we could have a single flat array for all evaluations, see
482        // same comment in combine_evaluations
483        let evals = combine_evaluations(evaluations, polyscale);
484        let blinding_commitment = srs.full_srs.h.mul(self.blinding);
485        // Taking the first element of the commitment, i.e. no support for
486        // chunking.
487        let divisor_commitment = srs
488            .verifier_srs
489            .commit_non_hiding(&divisor_polynomial(elm), 1)
490            .get_first_chunk();
491        // Taking the first element of the commitment, i.e. no support for
492        // chunking.
493        let eval_commitment = srs
494            .full_srs
495            .commit_non_hiding(&eval_polynomial(elm, &evals), 1)
496            .get_first_chunk()
497            .into_group();
498        let numerator_commitment = { poly_commitment - eval_commitment - blinding_commitment };
499        // We compute the result of the multiplication of two miller loop, to
500        // apply only one final exponentiation
501        let to_loop_left = [
502            ark_ec::pairing::prepare_g1::<Pair>(numerator_commitment),
503            // Note that we do a neagtion here, to put everything on the same
504            // side
505            ark_ec::pairing::prepare_g1::<Pair>(self.quotient.into_group().neg()),
506        ];
507        let to_loop_right = [
508            ark_ec::pairing::prepare_g2::<Pair>(Pair::G2Affine::generator()),
509            ark_ec::pairing::prepare_g2::<Pair>(divisor_commitment),
510        ];
511        // the result here is
512        // `numerator_commitment * 1 - quotient * divisor_commitment`.
513        // Note that the unwrap cannot fail as the output of a miller loop is
514        // non zero
515        let res = Pair::multi_pairing(to_loop_left, to_loop_right);
516
517        res.is_zero()
518    }
519}