Skip to main content

poly_commitment/
commitment.rs

1//! This module implements Dlog-based polynomial commitment schema.
2//! The following functionality is implemented
3//!
4//! 1. Commit to polynomial with its max degree
5//! 2. Open polynomial commitment batch at the given evaluation point and
6//!    scaling factor scalar producing the batched opening proof
7//! 3. Verify batch of batched opening proofs
8
9use ark_ec::{
10    models::short_weierstrass::Affine as SWJAffine, short_weierstrass::SWCurveConfig, AffineRepr,
11    CurveGroup, VariableBaseMSM,
12};
13use ark_ff::{BigInteger, Field, One, PrimeField, Zero};
14use ark_poly::univariate::DensePolynomial;
15use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
16use groupmap::{BWParameters, GroupMap};
17use mina_poseidon::{sponge::ScalarChallenge, FqSponge};
18use o1_utils::{field_helpers::product, ExtendedDensePolynomial as _};
19use rayon::prelude::*;
20use serde::{de::Visitor, Deserialize, Serialize};
21use serde_with::{
22    de::DeserializeAsWrap, ser::SerializeAsWrap, serde_as, DeserializeAs, SerializeAs,
23};
24use std::{
25    iter::Iterator,
26    marker::PhantomData,
27    ops::{Add, AddAssign, Sub},
28};
29
30/// Represent a polynomial commitment when the type is instantiated with a
31/// curve.
32///
33/// The structure also handles chunking, i.e. when we aim to handle polynomials
34/// whose degree is higher than the SRS size. For this reason, we do use a
35/// vector for the field `chunks`.
36///
37/// Note that the parameter `C` is not constrained to be a curve, therefore in
38/// some places in the code, `C` can refer to a scalar field element. For
39/// instance, `PolyComm<G::ScalarField>` is used to represent the evaluation of
40/// the polynomial bound by a specific commitment, at a particular evaluation
41/// point.
42#[serde_as]
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(bound = "C: CanonicalDeserialize + CanonicalSerialize")]
45pub struct PolyComm<C> {
46    #[serde_as(as = "Vec<o1_utils::serialization::SerdeAs>")]
47    pub chunks: Vec<C>,
48}
49
50impl<C> PolyComm<C>
51where
52    C: CommitmentCurve,
53{
54    /// Multiplies each commitment chunk of f with powers of zeta^n
55    #[must_use]
56    pub fn chunk_commitment(&self, zeta_n: C::ScalarField) -> Self {
57        let mut res = C::Group::zero();
58        // use Horner's to compute chunk[0] + z^n chunk[1] + z^2n chunk[2] + ...
59        // as ( chunk[-1] * z^n + chunk[-2] ) * z^n + chunk[-3]
60        // (https://en.wikipedia.org/wiki/Horner%27s_method)
61        for chunk in self.chunks.iter().rev() {
62            res *= zeta_n;
63            res.add_assign(chunk);
64        }
65
66        Self {
67            chunks: vec![res.into_affine()],
68        }
69    }
70}
71
72impl<F> PolyComm<F>
73where
74    F: Field,
75{
76    /// Multiplies each blinding chunk of f with powers of zeta^n
77    pub fn chunk_blinding(&self, zeta_n: F) -> F {
78        let mut res = F::zero();
79        // use Horner's to compute chunk[0] + z^n chunk[1] + z^2n chunk[2] + ...
80        // as ( chunk[-1] * z^n + chunk[-2] ) * z^n + chunk[-3]
81        // (https://en.wikipedia.org/wiki/Horner%27s_method)
82        for chunk in self.chunks.iter().rev() {
83            res *= zeta_n;
84            res += chunk;
85        }
86        res
87    }
88}
89
90impl<G> PolyComm<G> {
91    /// Returns an iterator over the chunks.
92    pub fn iter(&self) -> std::slice::Iter<'_, G> {
93        self.chunks.iter()
94    }
95}
96
97impl<'a, G> IntoIterator for &'a PolyComm<G> {
98    type Item = &'a G;
99    type IntoIter = std::slice::Iter<'a, G>;
100
101    fn into_iter(self) -> Self::IntoIter {
102        self.chunks.iter()
103    }
104}
105
106/// A commitment to a polynomial with some blinding factors.
107#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct BlindedCommitment<G>
109where
110    G: CommitmentCurve,
111{
112    pub commitment: PolyComm<G>,
113    pub blinders: PolyComm<G::ScalarField>,
114}
115
116impl<T> PolyComm<T> {
117    #[must_use]
118    pub const fn new(chunks: Vec<T>) -> Self {
119        Self { chunks }
120    }
121}
122
123impl<T, U> SerializeAs<PolyComm<T>> for PolyComm<U>
124where
125    U: SerializeAs<T>,
126{
127    fn serialize_as<S>(source: &PolyComm<T>, serializer: S) -> Result<S::Ok, S::Error>
128    where
129        S: serde::Serializer,
130    {
131        serializer.collect_seq(
132            source
133                .chunks
134                .iter()
135                .map(|e| SerializeAsWrap::<T, U>::new(e)),
136        )
137    }
138}
139
140impl<'de, T, U> DeserializeAs<'de, PolyComm<T>> for PolyComm<U>
141where
142    U: DeserializeAs<'de, T>,
143{
144    fn deserialize_as<D>(deserializer: D) -> Result<PolyComm<T>, D::Error>
145    where
146        D: serde::Deserializer<'de>,
147    {
148        struct SeqVisitor<T, U> {
149            marker: PhantomData<(T, U)>,
150        }
151
152        impl<'de, T, U> Visitor<'de> for SeqVisitor<T, U>
153        where
154            U: DeserializeAs<'de, T>,
155        {
156            type Value = PolyComm<T>;
157
158            fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159                formatter.write_str("a sequence")
160            }
161
162            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
163            where
164                A: serde::de::SeqAccess<'de>,
165            {
166                #[allow(clippy::redundant_closure_call)]
167                let mut chunks = vec![];
168
169                while let Some(value) = seq
170                    .next_element()?
171                    .map(|v: DeserializeAsWrap<T, U>| v.into_inner())
172                {
173                    chunks.push(value);
174                }
175
176                Ok(PolyComm::new(chunks))
177            }
178        }
179
180        let visitor = SeqVisitor::<T, U> {
181            marker: PhantomData,
182        };
183        deserializer.deserialize_seq(visitor)
184    }
185}
186
187impl<A: Copy + Clone + CanonicalDeserialize + CanonicalSerialize> PolyComm<A> {
188    pub fn map<B, F>(&self, mut f: F) -> PolyComm<B>
189    where
190        F: FnMut(A) -> B,
191        B: CanonicalDeserialize + CanonicalSerialize,
192    {
193        let chunks = self.chunks.iter().map(|x| f(*x)).collect();
194        PolyComm::new(chunks)
195    }
196
197    /// Returns the number of chunks.
198    #[must_use]
199    pub const fn len(&self) -> usize {
200        self.chunks.len()
201    }
202
203    /// Returns `true` if the commitment is empty.
204    #[must_use]
205    pub const fn is_empty(&self) -> bool {
206        self.chunks.is_empty()
207    }
208
209    // TODO: if all callers end up calling unwrap, just call this zip_eq and
210    // panic here (and document the panic)
211    #[must_use]
212    pub fn zip<B: Copy + CanonicalDeserialize + CanonicalSerialize>(
213        &self,
214        other: &PolyComm<B>,
215    ) -> Option<PolyComm<(A, B)>> {
216        if self.chunks.len() != other.chunks.len() {
217            return None;
218        }
219        let chunks = self
220            .chunks
221            .iter()
222            .zip(other.chunks.iter())
223            .map(|(x, y)| (*x, *y))
224            .collect();
225        Some(PolyComm::new(chunks))
226    }
227
228    /// Return only the first chunk.
229    ///
230    /// Getting this single value is relatively common in the codebase, even
231    /// though we should not do this, and abstract the chunks in the structure.
232    #[must_use]
233    pub fn get_first_chunk(&self) -> A {
234        self.chunks[0]
235    }
236}
237
238/// Inside the circuit, we have a specialized scalar multiplication which
239/// computes either
240///
241/// ```ignore
242/// |g: G, x: G::ScalarField| g.scale(x + 2^n)
243/// ```
244///
245/// if the scalar field of G is greater than the size of the base field
246/// and
247///
248/// ```ignore
249/// |g: G, x: G::ScalarField| g.scale(2*x + 2^n)
250/// ```
251///
252/// otherwise. So, if we want to actually scale by `x`, we need to apply the
253/// inverse function of `|x| x + 2^n` (or of `|x| 2*x + 2^n` in the other case),
254/// before supplying the scalar to our in-circuit scalar-multiplication
255/// function. This computes that inverse function.
256/// Namely,
257///
258/// ```ignore
259/// |x: G::ScalarField| x - 2^n
260/// ```
261///
262/// when the scalar field is larger than the base field and
263///
264/// ```ignore
265/// |x: G::ScalarField| (x - 2^n) / 2
266/// ```
267///
268/// in the other case.
269pub fn shift_scalar<G: AffineRepr>(x: G::ScalarField) -> G::ScalarField
270where
271    G::BaseField: PrimeField,
272{
273    let n1 = <G::ScalarField as PrimeField>::MODULUS;
274    let n2 = <G::ScalarField as PrimeField>::BigInt::from_bits_le(
275        &<G::BaseField as PrimeField>::MODULUS.to_bits_le()[..],
276    );
277    let two: G::ScalarField = (2u64).into();
278    let two_pow = two.pow([u64::from(<G::ScalarField as PrimeField>::MODULUS_BIT_SIZE)]);
279    if n1 < n2 {
280        (x - (two_pow + G::ScalarField::one())) / two
281    } else {
282        x - two_pow
283    }
284}
285
286impl<'a, C: AffineRepr> Add<&'a PolyComm<C>> for &PolyComm<C> {
287    type Output = PolyComm<C>;
288
289    fn add(self, other: &'a PolyComm<C>) -> PolyComm<C> {
290        let mut chunks = vec![];
291        let n1 = self.chunks.len();
292        let n2 = other.chunks.len();
293        for i in 0..std::cmp::max(n1, n2) {
294            let pt = if i < n1 && i < n2 {
295                (self.chunks[i] + other.chunks[i]).into_affine()
296            } else if i < n1 {
297                self.chunks[i]
298            } else {
299                other.chunks[i]
300            };
301            chunks.push(pt);
302        }
303        PolyComm::new(chunks)
304    }
305}
306
307impl<'a, C: AffineRepr + Sub<Output = C::Group>> Sub<&'a PolyComm<C>> for &PolyComm<C> {
308    type Output = PolyComm<C>;
309
310    fn sub(self, other: &'a PolyComm<C>) -> PolyComm<C> {
311        let mut chunks = vec![];
312        let n1 = self.chunks.len();
313        let n2 = other.chunks.len();
314        for i in 0..std::cmp::max(n1, n2) {
315            let pt = if i < n1 && i < n2 {
316                (self.chunks[i] - other.chunks[i]).into_affine()
317            } else if i < n1 {
318                self.chunks[i]
319            } else {
320                other.chunks[i]
321            };
322            chunks.push(pt);
323        }
324        PolyComm::new(chunks)
325    }
326}
327
328impl<C: AffineRepr> PolyComm<C> {
329    #[must_use]
330    pub fn scale(&self, c: C::ScalarField) -> Self {
331        Self {
332            chunks: self.chunks.iter().map(|g| g.mul(c).into_affine()).collect(),
333        }
334    }
335
336    /// Performs a multi-scalar multiplication between scalars `elm` and
337    /// commitments `com`.
338    ///
339    /// If both are empty, returns a commitment of length 1
340    /// containing the point at infinity.
341    ///
342    /// ## Panics
343    ///
344    /// Panics if `com` and `elm` are not of the same size.
345    #[must_use]
346    pub fn multi_scalar_mul(com: &[&Self], elm: &[C::ScalarField]) -> Self {
347        assert_eq!(com.len(), elm.len());
348
349        if com.is_empty() || elm.is_empty() {
350            return Self::new(vec![C::zero()]);
351        }
352
353        let all_scalars: Vec<_> = elm.iter().map(|s| s.into_bigint()).collect();
354
355        let elems_size = Iterator::max(com.iter().map(|c| c.chunks.len())).unwrap();
356
357        let chunks = (0..elems_size)
358            .map(|chunk| {
359                let (points, scalars): (Vec<_>, Vec<_>) = com
360                    .iter()
361                    .zip(&all_scalars)
362                    // get rid of scalars that don't have an associated chunk
363                    .filter_map(|(com, scalar)| com.chunks.get(chunk).map(|c| (c, scalar)))
364                    .unzip();
365
366                // Splitting into 2 chunks seems optimal; but in
367                // practice elems_size is almost always 1
368                //
369                // (see the comment to the `benchmark_msm_parallel_vesta` MSM benchmark)
370                let subchunk_size = std::cmp::max(points.len() / 2, 1);
371
372                points
373                    .into_par_iter()
374                    .chunks(subchunk_size)
375                    .zip(scalars.into_par_iter().chunks(subchunk_size))
376                    .map(|(psc, ssc)| C::Group::msm_bigint(&psc, &ssc).into_affine())
377                    .reduce(C::zero, |x, y| (x + y).into())
378            })
379            .collect();
380
381        Self::new(chunks)
382    }
383}
384
385/// Evaluates the challenge polynomial `b(X)` at point `x`.
386///
387/// The challenge polynomial is defined as:
388/// ```text
389/// b(X) = prod_{i=0}^{k-1} (1 + u_{k-i} * X^{2^i})
390/// ```
391/// where `chals = [u_1, ..., u_k]` are the IPA challenges.
392///
393/// This polynomial was introduced in Section 3.2 of the
394/// [Halo paper](https://eprint.iacr.org/2019/1021.pdf) as part of the Inner Product
395/// Argument. It efficiently encodes the folded evaluation point: after `k` rounds
396/// of IPA, the evaluation point vector is reduced to a single value `b(x)`.
397///
398/// The polynomial has degree `2^k - 1` and can be evaluated in `O(k)` field
399/// operations using the product form above. Its coefficients can be computed
400/// using [`b_poly_coefficients`].
401///
402/// # Usage
403///
404/// - In the verifier (`ipa.rs`), `b_poly` is called to compute evaluations at
405///   the challenge points (e.g., `zeta`, `zeta * omega`).
406/// - In `RecursionChallenge::evals`, it computes evaluations for the batched
407///   polynomial commitment check.
408///
409/// # References
410/// - [Halo paper, Section 3.2](https://eprint.iacr.org/2019/1021.pdf) - original
411///   definition of the challenge polynomial
412/// - [PCD paper, Appendix A.2, Step 8](https://eprint.iacr.org/2020/499) - use in
413///   accumulation context
414pub fn b_poly<F: Field>(chals: &[F], x: F) -> F {
415    let k = chals.len();
416
417    let mut pow_twos = vec![x];
418
419    for i in 1..k {
420        pow_twos.push(pow_twos[i - 1].square());
421    }
422
423    product((0..k).map(|i| F::one() + (chals[i] * pow_twos[k - 1 - i])))
424}
425
426/// Computes the coefficients of the challenge polynomial `b(X)`.
427///
428/// Given IPA challenges `chals = [u_1, ..., u_k]`, returns a vector
429/// `[s_0, s_1, ..., s_{2^k - 1}]` such that:
430/// ```text
431/// b(X) = sum_{i=0}^{2^k - 1} s_i * X^i
432/// ```
433///
434/// The coefficients have a closed form: for each index `i` with bit decomposition
435/// `i = sum_j b_j * 2^j`, the coefficient is `s_i = prod_{j: b_j = 1} u_{k-j}`.
436///
437/// # Usage
438///
439/// This function is used when the full coefficient vector is needed:
440/// - In `SRS::verify` (`ipa.rs`): the coefficients are computed and included in
441///   the batched MSM to verify the accumulated commitment `<s, G>`.
442/// - In `RecursionChallenge::evals`: for chunked polynomial evaluation when the
443///   degree exceeds `max_poly_size`.
444///
445/// # Complexity
446///
447/// Returns `2^k` coefficients. The MSM `<s, G>` using these coefficients is the
448/// expensive operation that gets deferred in recursive proof composition.
449///
450/// # References
451/// - [Halo paper, Section 3.2](https://eprint.iacr.org/2019/1021.pdf)
452pub fn b_poly_coefficients<F: Field>(chals: &[F]) -> Vec<F> {
453    let rounds = chals.len();
454    let s_length = 1 << rounds;
455    let mut s = vec![F::one(); s_length];
456    let mut k: usize = 0;
457    let mut pow: usize = 1;
458    for i in 1..s_length {
459        k += usize::from(i == pow);
460        pow <<= u32::from(i == pow);
461        s[i] = s[i - (pow >> 1)] * chals[rounds - 1 - (k - 1)];
462    }
463    s
464}
465
466pub fn squeeze_prechallenge<
467    const FULL_ROUNDS: usize,
468    Fq: Field,
469    G,
470    Fr: Field,
471    EFqSponge: FqSponge<Fq, G, Fr, FULL_ROUNDS>,
472>(
473    sponge: &mut EFqSponge,
474) -> ScalarChallenge<Fr> {
475    ScalarChallenge::new(sponge.challenge())
476}
477
478pub fn squeeze_challenge<
479    const FULL_ROUNDS: usize,
480    Fq: Field,
481    G,
482    Fr: PrimeField,
483    EFqSponge: FqSponge<Fq, G, Fr, FULL_ROUNDS>,
484>(
485    endo_r: &Fr,
486    sponge: &mut EFqSponge,
487) -> Fr {
488    squeeze_prechallenge(sponge).to_field(endo_r)
489}
490
491pub fn absorb_commitment<
492    const FULL_ROUNDS: usize,
493    Fq: Field,
494    G: Clone,
495    Fr: PrimeField,
496    EFqSponge: FqSponge<Fq, G, Fr, FULL_ROUNDS>,
497>(
498    sponge: &mut EFqSponge,
499    commitment: &PolyComm<G>,
500) {
501    sponge.absorb_g(&commitment.chunks);
502}
503
504/// A useful trait extending [`AffineRepr`] for commitments.
505///
506/// Unfortunately, we can't specify that `AffineRepr<BaseField : PrimeField>`,
507/// so usage of this traits must manually bind `G::BaseField: PrimeField`.
508pub trait CommitmentCurve: AffineRepr + Sub<Output = Self::Group> {
509    type Params: SWCurveConfig;
510    type Map: GroupMap<Self::BaseField>;
511
512    fn to_coordinates(&self) -> Option<(Self::BaseField, Self::BaseField)>;
513    fn of_coordinates(x: Self::BaseField, y: Self::BaseField) -> Self;
514}
515
516/// A trait extending [`CommitmentCurve`] for endomorphisms.
517///
518/// Unfortunately, we can't specify that `AffineRepr<BaseField : PrimeField>`,
519/// so usage of this traits must manually bind `G::BaseField: PrimeField`.
520pub trait EndoCurve: CommitmentCurve {
521    /// Combine where x1 = one
522    fn combine_one(g1: &[Self], g2: &[Self], x2: Self::ScalarField) -> Vec<Self> {
523        crate::combine::window_combine(g1, g2, Self::ScalarField::one(), x2)
524    }
525
526    /// Combine where x1 = one
527    fn combine_one_endo(
528        endo_r: Self::ScalarField,
529        _endo_q: Self::BaseField,
530        g1: &[Self],
531        g2: &[Self],
532        x2: &ScalarChallenge<Self::ScalarField>,
533    ) -> Vec<Self> {
534        crate::combine::window_combine(g1, g2, Self::ScalarField::one(), x2.to_field(&endo_r))
535    }
536
537    fn combine(
538        g1: &[Self],
539        g2: &[Self],
540        x1: Self::ScalarField,
541        x2: Self::ScalarField,
542    ) -> Vec<Self> {
543        crate::combine::window_combine(g1, g2, x1, x2)
544    }
545}
546
547impl<P: SWCurveConfig + Clone> CommitmentCurve for SWJAffine<P> {
548    type Params = P;
549    type Map = BWParameters<P>;
550
551    fn to_coordinates(&self) -> Option<(Self::BaseField, Self::BaseField)> {
552        if self.infinity {
553            None
554        } else {
555            Some((self.x, self.y))
556        }
557    }
558
559    fn of_coordinates(x: P::BaseField, y: P::BaseField) -> Self {
560        Self::new_unchecked(x, y)
561    }
562}
563
564impl<P: SWCurveConfig + Clone> EndoCurve for SWJAffine<P> {
565    fn combine_one(g1: &[Self], g2: &[Self], x2: Self::ScalarField) -> Vec<Self> {
566        crate::combine::affine_window_combine_one(g1, g2, x2)
567    }
568
569    fn combine_one_endo(
570        _endo_r: Self::ScalarField,
571        endo_q: Self::BaseField,
572        g1: &[Self],
573        g2: &[Self],
574        x2: &ScalarChallenge<Self::ScalarField>,
575    ) -> Vec<Self> {
576        crate::combine::affine_window_combine_one_endo(endo_q, g1, g2, x2)
577    }
578
579    fn combine(
580        g1: &[Self],
581        g2: &[Self],
582        x1: Self::ScalarField,
583        x2: Self::ScalarField,
584    ) -> Vec<Self> {
585        crate::combine::affine_window_combine(g1, g2, x1, x2)
586    }
587}
588
589/// Computes the linearization of evaluations.
590///
591/// Handles a (potentially split) polynomial.
592/// Each polynomial in `polys` is represented by a matrix where the
593/// rows correspond to evaluated points, and the columns represent
594/// potential segments (if a polynomial was split in several parts).
595///
596/// Elements in `evaluation_points` are several discrete points on which
597/// we evaluate polynomials, e.g. `[zeta,zeta*w]`. See `PointEvaluations`.
598///
599/// Note that if one of the polynomial comes specified with a degree
600/// bound, the evaluation for the last segment is potentially shifted
601/// to meet the proof.
602///
603/// Returns
604/// ```text
605/// |polys| |segments[k]|
606///    Σ         Σ         polyscale^{k*n+i} (Σ polys[k][j][i] * evalscale^j)
607///  k = 1     i = 1                          j
608/// ```
609#[allow(clippy::type_complexity)]
610pub fn combined_inner_product<F: PrimeField>(
611    polyscale: &F,
612    evalscale: &F,
613    // TODO(mimoo): needs a type that can get you evaluations or segments
614    polys: &[Vec<Vec<F>>],
615) -> F {
616    // final combined evaluation result
617    let mut res = F::zero();
618    // polyscale^i
619    let mut polyscale_i = F::one();
620
621    for evals_tr in polys.iter().filter(|evals_tr| !evals_tr[0].is_empty()) {
622        // Transpose the evaluations.
623        // evals[i] = {evals_tr[j][i]}_j now corresponds to a column in
624        // evals_tr, representing a segment.
625        let evals: Vec<_> = (0..evals_tr[0].len())
626            .map(|i| evals_tr.iter().map(|v| v[i]).collect::<Vec<_>>())
627            .collect();
628
629        // Iterating over the polynomial segments.
630        // Each segment gets its own polyscale^i, each segment element j is
631        // multiplied by evalscale^j. Given that polyscale_i = polyscale^i0 at
632        // this point, after this loop we have:
633        //
634        //    res += Σ polyscale^{i0+i} ( Σ evals_tr[j][i] * evalscale^j )
635        //           i                    j
636        //
637        for eval in &evals {
638            // p_i(evalscale)
639            let term = DensePolynomial::<F>::eval_polynomial(eval, *evalscale);
640            res += &(polyscale_i * term);
641            polyscale_i *= polyscale;
642        }
643    }
644    res
645}
646
647/// Contains the evaluation of a polynomial commitment at a set of points.
648pub struct Evaluation<G>
649where
650    G: AffineRepr,
651{
652    /// The commitment of the polynomial being evaluated.
653    ///
654    /// Note that [`PolyComm`] contains a vector of commitments, which handles
655    /// the case when chunking is used, i.e. when the polynomial degree is
656    /// higher than the SRS size.
657    pub commitment: PolyComm<G>,
658
659    /// Contains an evaluation table.
660    ///
661    /// For instance, for vanilla `PlonK`, it would be a vector of (chunked)
662    /// evaluations at zeta and zeta*omega. The outer vector would be the
663    /// evaluations at the different points (e.g. zeta and zeta*omega for
664    /// vanilla `PlonK`) and the inner vector would be the chunks of the
665    /// polynomial.
666    pub evaluations: Vec<Vec<G::ScalarField>>,
667}
668
669/// Contains the batch evaluation
670pub struct BatchEvaluationProof<'a, G, EFqSponge, OpeningProof, const FULL_ROUNDS: usize>
671where
672    G: AffineRepr,
673    EFqSponge: FqSponge<G::BaseField, G, G::ScalarField, FULL_ROUNDS>,
674{
675    /// Sponge used to coin and absorb values and simulate
676    /// non-interactivity using the Fiat-Shamir transformation.
677    pub sponge: EFqSponge,
678    /// A list of evaluations, each supposed to correspond to a different
679    /// polynomial.
680    pub evaluations: Vec<Evaluation<G>>,
681    /// The actual evaluation points. Each field `evaluations` of each structure
682    /// of `Evaluation` should have the same (outer) length.
683    pub evaluation_points: Vec<G::ScalarField>,
684    /// A challenge to combine polynomials. Powers of this point will be used,
685    /// hence the name.
686    pub polyscale: G::ScalarField,
687    /// A challenge to aggregate multiple evaluation points.
688    pub evalscale: G::ScalarField,
689    /// The opening proof.
690    pub opening: &'a OpeningProof,
691    pub combined_inner_product: G::ScalarField,
692}
693
694/// Populates the parameters `scalars` and `points`.
695///
696/// It iterates over the evaluations and adds each commitment to the
697/// vector `points`.
698/// The parameter `scalars` is populated with the values:
699/// `rand_base * polyscale^i` for each commitment.
700/// For instance, if we have 3 commitments, the `scalars` vector will
701/// contain the values
702/// ```text
703/// [rand_base, rand_base * polyscale, rand_base * polyscale^2]
704/// ```
705/// and the vector `points` will contain the commitments.
706///
707/// Note that the function skips the commitments that are empty.
708///
709/// If more than one commitment is present in a single evaluation (i.e. if
710/// `elems` is larger than one), it means that probably chunking was used (i.e.
711/// it is a commitment to a polynomial larger than the SRS).
712pub fn combine_commitments<G: CommitmentCurve>(
713    evaluations: &[Evaluation<G>],
714    scalars: &mut Vec<G::ScalarField>,
715    points: &mut Vec<G>,
716    polyscale: G::ScalarField,
717    rand_base: G::ScalarField,
718) {
719    // will contain the power of polyscale
720    let mut polyscale_i = G::ScalarField::one();
721
722    for Evaluation { commitment, .. } in evaluations.iter().filter(|x| !x.commitment.is_empty()) {
723        // iterating over the polynomial segments
724        for comm_ch in &commitment.chunks {
725            scalars.push(rand_base * polyscale_i);
726            points.push(*comm_ch);
727
728            // compute next power of polyscale
729            polyscale_i *= polyscale;
730        }
731    }
732}
733
734#[cfg(feature = "ocaml_types")]
735#[allow(non_local_definitions)]
736pub mod caml {
737    // polynomial commitment
738    use super::PolyComm;
739    use ark_ec::AffineRepr;
740
741    #[derive(Clone, Debug, ocaml::IntoValue, ocaml::FromValue, ocaml_gen::Struct)]
742    pub struct CamlPolyComm<CamlG> {
743        pub unshifted: Vec<CamlG>,
744        pub shifted: Option<CamlG>,
745    }
746
747    // handy conversions
748
749    impl<G, CamlG> From<PolyComm<G>> for CamlPolyComm<CamlG>
750    where
751        G: AffineRepr,
752        CamlG: From<G>,
753    {
754        fn from(polycomm: PolyComm<G>) -> Self {
755            Self {
756                unshifted: polycomm.chunks.into_iter().map(CamlG::from).collect(),
757                shifted: None,
758            }
759        }
760    }
761
762    impl<'a, G, CamlG> From<&'a PolyComm<G>> for CamlPolyComm<CamlG>
763    where
764        G: AffineRepr,
765        CamlG: From<G> + From<&'a G>,
766    {
767        fn from(polycomm: &'a PolyComm<G>) -> Self {
768            Self {
769                unshifted: polycomm.chunks.iter().map(Into::<CamlG>::into).collect(),
770                shifted: None,
771            }
772        }
773    }
774
775    impl<G, CamlG> From<CamlPolyComm<CamlG>> for PolyComm<G>
776    where
777        G: AffineRepr + From<CamlG>,
778    {
779        fn from(camlpolycomm: CamlPolyComm<CamlG>) -> Self {
780            assert!(
781                camlpolycomm.shifted.is_none(),
782                "mina#14628: Shifted commitments are deprecated and must not be used"
783            );
784            Self {
785                chunks: camlpolycomm
786                    .unshifted
787                    .into_iter()
788                    .map(Into::<G>::into)
789                    .collect(),
790            }
791        }
792    }
793
794    impl<'a, G, CamlG> From<&'a CamlPolyComm<CamlG>> for PolyComm<G>
795    where
796        G: AffineRepr + From<&'a CamlG> + From<CamlG>,
797    {
798        fn from(camlpolycomm: &'a CamlPolyComm<CamlG>) -> Self {
799            assert!(
800                camlpolycomm.shifted.is_none(),
801                "mina#14628: Shifted commitments are deprecated and must not be used"
802            );
803            Self {
804                //FIXME something with as_ref()
805                chunks: camlpolycomm.unshifted.iter().map(Into::into).collect(),
806            }
807        }
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use mina_curves::pasta::Fp;
815    use std::str::FromStr;
816
817    /// Regression test for `b_poly` evaluation.
818    ///
819    /// The challenge polynomial b(X) is defined as:
820    /// ```text
821    /// b(X) = prod_{i=0}^{k-1} (1 + u_{k-i} * X^{2^i})
822    /// ```
823    /// See Section 3.2 of the [Halo paper](https://eprint.iacr.org/2019/1021.pdf).
824    #[test]
825    fn test_b_poly_regression() {
826        // 15 challenges (typical for domain size 2^15)
827        let chals: Vec<Fp> = (2u64..=16).map(Fp::from).collect();
828
829        let zeta = Fp::from(17u64);
830        let zeta_omega = Fp::from(19u64);
831
832        let b_at_zeta = b_poly(&chals, zeta);
833        let b_at_zeta_omega = b_poly(&chals, zeta_omega);
834
835        // Expected values (computed and verified)
836        let expected_at_zeta = Fp::from_str(
837            "21115683812642620361045381629886583866877919362491419134086003378733605776328",
838        )
839        .unwrap();
840        let expected_at_zeta_omega = Fp::from_str(
841            "2298325069360593860729719174291433577456794311517767070156020442825391962511",
842        )
843        .unwrap();
844
845        assert_eq!(b_at_zeta, expected_at_zeta, "b(zeta) mismatch");
846        assert_eq!(
847            b_at_zeta_omega, expected_at_zeta_omega,
848            "b(zeta*omega) mismatch"
849        );
850    }
851
852    /// Regression test for `b_poly_coefficients`.
853    ///
854    /// Verifies that the coefficients satisfy: `b(x) = sum_i s_i * x^i`
855    /// where `s_i = prod_{j: bit_j(i) = 1} u_{k-j}`
856    #[test]
857    fn test_b_poly_coefficients_regression() {
858        // Use 4 challenges for manageable output (2^4 = 16 coefficients)
859        let chals: Vec<Fp> = vec![
860            Fp::from(2u64),
861            Fp::from(3u64),
862            Fp::from(5u64),
863            Fp::from(7u64),
864        ];
865
866        let coeffs = b_poly_coefficients(&chals);
867
868        assert_eq!(coeffs.len(), 16, "Should have 2^4 = 16 coefficients");
869
870        // Expected coefficients (computed and verified)
871        // s_i = prod_{j: bit_j(i) = 1} u_{k-j}
872        // chals = [2, 3, 5, 7] means u_1=2, u_2=3, u_3=5, u_4=7
873        // Index 0 (0000): 1
874        // Index 1 (0001): u_4 = 7
875        // Index 2 (0010): u_3 = 5
876        // Index 3 (0011): u_3 * u_4 = 35
877        // etc.
878        let expected: Vec<Fp> = vec![
879            Fp::from(1u64),   // 0: 1
880            Fp::from(7u64),   // 1: u_4
881            Fp::from(5u64),   // 2: u_3
882            Fp::from(35u64),  // 3: u_3 * u_4
883            Fp::from(3u64),   // 4: u_2
884            Fp::from(21u64),  // 5: u_2 * u_4
885            Fp::from(15u64),  // 6: u_2 * u_3
886            Fp::from(105u64), // 7: u_2 * u_3 * u_4
887            Fp::from(2u64),   // 8: u_1
888            Fp::from(14u64),  // 9: u_1 * u_4
889            Fp::from(10u64),  // 10: u_1 * u_3
890            Fp::from(70u64),  // 11: u_1 * u_3 * u_4
891            Fp::from(6u64),   // 12: u_1 * u_2
892            Fp::from(42u64),  // 13: u_1 * u_2 * u_4
893            Fp::from(30u64),  // 14: u_1 * u_2 * u_3
894            Fp::from(210u64), // 15: u_1 * u_2 * u_3 * u_4
895        ];
896
897        assert_eq!(coeffs, expected, "Coefficients mismatch");
898    }
899
900    /// Test that `b_poly` and `b_poly_coefficients` are consistent:
901    /// evaluating b(x) via the product form should equal evaluating via coefficients.
902    #[test]
903    fn test_b_poly_consistency() {
904        let chals: Vec<Fp> = (2u64..=10).map(Fp::from).collect();
905        let coeffs = b_poly_coefficients(&chals);
906
907        // Test at several points
908        for x_val in [1u64, 7, 13, 42, 100] {
909            let x = Fp::from(x_val);
910
911            // Evaluate via product form
912            let b_product = b_poly(&chals, x);
913
914            // Evaluate via coefficients: sum_i s_i * x^i
915            let mut b_coeffs = Fp::zero();
916            let mut x_pow = Fp::one();
917            for coeff in &coeffs {
918                b_coeffs += *coeff * x_pow;
919                x_pow *= x;
920            }
921
922            assert_eq!(
923                b_product, b_coeffs,
924                "b_poly and b_poly_coefficients inconsistent at x={x_val}"
925            );
926        }
927    }
928}