Skip to main content

snarkvm_algorithms/polycommit/sonic_pc/
mod.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::{
17    AlgebraicSponge,
18    fft::DensePolynomial,
19    msm::variable_base::VariableBase,
20    polycommit::{PCError, kzg10},
21    srs::{UniversalProver, UniversalVerifier},
22};
23use hashbrown::HashMap;
24use itertools::Itertools;
25use snarkvm_curves::traits::{AffineCurve, PairingCurve, PairingEngine, ProjectiveCurve};
26use snarkvm_fields::{One, Zero};
27
28use anyhow::{Result, bail, ensure};
29use rand::{Rng, SeedableRng};
30use std::{
31    borrow::Borrow,
32    collections::{BTreeMap, BTreeSet},
33    convert::TryInto,
34    marker::PhantomData,
35    ops::Mul,
36};
37
38mod data_structures;
39pub use data_structures::*;
40
41mod polynomial;
42pub use polynomial::*;
43
44/// A linear combination whose polynomial has not been formed.
45///
46/// `open_combinations` resolves each combination to the polynomials it names
47/// and the scalar in front of each, and leaves the coefficient arithmetic to
48/// `open_lazy_combinations`, which folds in the Fiat-Shamir challenge and
49/// accumulates once. Holding the polynomial by reference is the
50/// point: `LabeledPolynomial` owns a `Cow<'static, _>`, so putting one into a
51/// combination is a clone of every coefficient.
52struct LazyCombination<'a, E: PairingEngine> {
53    label: String,
54    /// `(coefficient, polynomial)` per term, with the `LCTerm::One` constants
55    /// already filtered out -- the verifier uses those directly and nothing
56    /// is committed to them.
57    terms: Vec<(E::Fr, &'a LabeledPolynomial<E::Fr>)>,
58    /// `Some` only for a single-term combination whose coefficient is one,
59    /// which is what `open_combinations` enforces; `open_lazy_combinations`
60    /// relies on it.
61    degree_bound: Option<usize>,
62    randomness: Randomness<E>,
63}
64
65/// Polynomial commitment based on [\[KZG10\]][kzg], with degree enforcement and
66/// batching taken from [[MBKM19, “Sonic”]][sonic] (more precisely, their
67/// counterparts in [[Gabizon19, “AuroraLight”]][al] that avoid negative G1
68/// powers). The (optional) hiding property of the commitment scheme follows the
69/// approach described in [[CHMMVW20, “Marlin”]][marlin].
70///
71/// [kzg]: http://cacr.uwaterloo.ca/techreports/2010/cacr2010-10.pdf
72/// [sonic]: https://eprint.iacr.org/2019/099
73/// [al]: https://eprint.iacr.org/2019/601
74/// [marlin]: https://eprint.iacr.org/2019/1047
75#[derive(Clone, Debug)]
76pub struct SonicKZG10<E: PairingEngine, S: AlgebraicSponge<E::Fq, 2>> {
77    _engine: PhantomData<(E, S)>,
78}
79
80impl<E: PairingEngine, S: AlgebraicSponge<E::Fq, 2>> SonicKZG10<E, S> {
81    pub fn load_srs(max_degree: usize) -> Result<UniversalParams<E>, PCError> {
82        kzg10::KZG10::load_srs(max_degree)
83    }
84
85    pub fn trim(
86        pp: &UniversalParams<E>,
87        supported_degree: usize,
88        supported_lagrange_sizes: impl IntoIterator<Item = usize>,
89        supported_hiding_bound: usize,
90        enforced_degree_bounds: Option<&[usize]>,
91    ) -> Result<(CommitterKey<E>, UniversalVerifier<E>)> {
92        let trim_time = start_timer!(|| "Trimming public parameters");
93        let max_degree = pp.max_degree();
94
95        let enforced_degree_bounds = enforced_degree_bounds.map(|bounds| {
96            let mut v = bounds.to_vec();
97            v.sort_unstable();
98            v.dedup();
99            v
100        });
101
102        // Every argument is validated before anything is downloaded, so a
103        // rejected call leaves the SRS as it was.
104        let supported_lagrange_sizes: Vec<usize> = supported_lagrange_sizes.into_iter().collect();
105        for &size in &supported_lagrange_sizes {
106            if !size.is_power_of_two() {
107                bail!("The Lagrange basis size ({size}) is not a power of two")
108            }
109            if size > max_degree + 1 {
110                bail!("The Lagrange basis size ({size}) is larger than the supported degree ({})", max_degree + 1)
111            }
112        }
113        if let Some([.., highest_enforced_degree_bound]) = enforced_degree_bounds.as_deref()
114            && *highest_enforced_degree_bound > supported_degree
115        {
116            bail!(
117                "The highest enforced degree bound {highest_enforced_degree_bound} is larger than the supported degree {supported_degree}"
118            );
119        }
120
121        // Every prefix download before anything is shared: a later one would
122        // replace a snapshot this key already shared, which the key would then
123        // keep alive after the SRS moved on. The suffix is downloaded, and shared,
124        // below, and touches only the suffix.
125        pp.download_powers_for(0..supported_degree + 1)?;
126        for &size in &supported_lagrange_sizes {
127            pp.download_powers_for(0..size)?;
128        }
129
130        let (shifted_powers_of_beta_g, shifted_powers_of_beta_times_gamma_g) = match enforced_degree_bounds.as_deref() {
131            // The bounds are sorted, so the last is the highest.
132            Some(enforced_degree_bounds @ [.., highest_enforced_degree_bound]) => {
133                let lowest_shift_degree = max_degree - highest_enforced_degree_bound;
134
135                let shifted_ck_time = start_timer!(|| format!(
136                    "Constructing `shifted_powers_of_beta_g` of size {}",
137                    max_degree - lowest_shift_degree + 1
138                ));
139
140                let (store, range) = pp.shared_powers_of_beta_g(lowest_shift_degree, pp.max_degree() + 1)?;
141                let shifted_powers_of_beta_g = Bases::shared(store, range);
142                let mut shifted_powers_of_beta_times_gamma_g = BTreeMap::new();
143                // Also add degree 0.
144                for degree_bound in enforced_degree_bounds {
145                    let shift_degree = max_degree - degree_bound;
146                    // We have an additional degree in `powers_of_beta_times_gamma_g` beyond
147                    // `powers_of_beta_g`.
148                    let powers_for_degree_bound = pp
149                        .powers_of_beta_times_gamma_g()
150                        .range(shift_degree..max_degree.min(shift_degree + supported_hiding_bound) + 2)
151                        .map(|(_k, v)| *v)
152                        .collect();
153                    shifted_powers_of_beta_times_gamma_g.insert(*degree_bound, powers_for_degree_bound);
154                }
155
156                end_timer!(shifted_ck_time);
157
158                (Some(shifted_powers_of_beta_g), Some(shifted_powers_of_beta_times_gamma_g))
159            }
160            _ => (None, None),
161        };
162
163        let (store, range) = pp.shared_powers_of_beta_g(0, supported_degree + 1)?;
164        let powers_of_beta_g = Bases::shared(store, range);
165        let powers_of_beta_times_gamma_g = pp
166            .powers_of_beta_times_gamma_g()
167            .range(0..=(supported_hiding_bound + 1))
168            .map(|(_k, v)| *v)
169            .collect::<Vec<_>>();
170        if powers_of_beta_times_gamma_g.len() != supported_hiding_bound + 2 {
171            return Err(
172                PCError::HidingBoundToolarge { hiding_poly_degree: supported_hiding_bound, num_powers: 0 }.into()
173            );
174        }
175
176        let mut lagrange_bases_at_beta_g = BTreeMap::new();
177        for size in supported_lagrange_sizes {
178            let lagrange_time = start_timer!(|| format!("Constructing `lagrange_bases` of size {size}"));
179            // Validated above: the size is a power of two no larger than the SRS.
180            let domain = crate::fft::EvaluationDomain::new(size).unwrap();
181            let lagrange_basis_at_beta_g = pp.lagrange_basis(domain)?;
182            assert!(lagrange_basis_at_beta_g.len().is_power_of_two());
183            lagrange_bases_at_beta_g.insert(domain.size(), lagrange_basis_at_beta_g);
184            end_timer!(lagrange_time);
185        }
186
187        let ck = CommitterKey {
188            powers_of_beta_g,
189            lagrange_bases_at_beta_g,
190            powers_of_beta_times_gamma_g,
191            shifted_powers_of_beta_g,
192            shifted_powers_of_beta_times_gamma_g,
193            enforced_degree_bounds,
194        };
195
196        let vk = pp.to_universal_verifier()?;
197
198        end_timer!(trim_time);
199        Ok((ck, vk))
200    }
201
202    /// Outputs commitments to `polynomials`.
203    ///
204    /// If `polynomials[i].is_hiding()`, then the `i`-th commitment is hiding
205    /// up to `polynomials.hiding_bound()` queries.
206    ///
207    /// `rng` should not be `None` if `polynomials[i].is_hiding() == true` for
208    /// any `i`.
209    ///
210    /// If for some `i`, `polynomials[i].is_hiding() == false`, then the
211    /// corresponding randomness is `Randomness<E>::empty()`.
212    ///
213    /// If for some `i`, `polynomials[i].degree_bound().is_some()`, then that
214    /// polynomial will have the corresponding degree bound enforced.
215    #[allow(clippy::format_push_string)]
216    pub fn commit<'b>(
217        universal_prover: &UniversalProver<E>,
218        ck: &CommitterUnionKey<E>,
219        polynomials: impl IntoIterator<Item = LabeledPolynomialWithBasis<'b, E::Fr>>,
220        mut rng: Option<&mut dyn Rng>,
221    ) -> Result<(Vec<LabeledCommitment<Commitment<E>>>, Vec<Randomness<E>>), PCError> {
222        let commit_time = start_timer!(|| "Committing to polynomials");
223
224        let mut pool = snarkvm_utilities::ExecutionPool::<Result<_, _>>::new();
225        for p in polynomials {
226            let seed = rng.as_mut().map(|r| {
227                let mut seed = [0u8; 32];
228                r.fill_bytes(&mut seed);
229                seed
230            });
231
232            kzg10::KZG10::<E>::check_degrees_and_bounds(
233                universal_prover.max_degree,
234                ck.enforced_degree_bounds.as_deref(),
235                p.clone(),
236            )?;
237            let degree_bound = p.degree_bound();
238            let hiding_bound = p.hiding_bound();
239            let label = p.label().to_string();
240
241            pool.add_job(move || {
242                let mut rng = seed.map(rand::rngs::StdRng::from_seed);
243                add_to_trace!(|| "PC::Commit", || format!(
244                    "Polynomial {} of degree {}, degree bound {:?}, and hiding bound {:?}",
245                    label,
246                    p.degree(),
247                    degree_bound,
248                    hiding_bound,
249                ));
250
251                let (comm, rand) = {
252                    let rng_ref = rng.as_mut().map(|s| s as _);
253                    match p.polynomial {
254                        PolynomialWithBasis::Lagrange { evaluations } => {
255                            let domain = crate::fft::EvaluationDomain::new(evaluations.evaluations.len()).unwrap();
256                            let lagrange_basis = ck
257                                .lagrange_basis(domain)
258                                .ok_or(PCError::UnsupportedLagrangeBasisSize(domain.size()))?;
259                            assert!(domain.size().is_power_of_two());
260                            assert!(lagrange_basis.size().is_power_of_two());
261                            kzg10::KZG10::commit_lagrange(
262                                &lagrange_basis,
263                                &evaluations.evaluations,
264                                hiding_bound,
265                                rng_ref,
266                            )?
267                        }
268                        PolynomialWithBasis::Monomial { polynomial, degree_bound } => {
269                            let powers = if let Some(degree_bound) = degree_bound {
270                                ck.shifted_powers_of_beta_g(degree_bound).unwrap()
271                            } else {
272                                ck.powers()
273                            };
274
275                            kzg10::KZG10::commit(&powers, &polynomial, hiding_bound, rng_ref)?
276                        }
277                    }
278                };
279
280                Ok((LabeledCommitment::new(label.to_string(), comm, degree_bound), rand))
281            });
282        }
283        let results: Vec<Result<_, PCError>> = pool.execute_all();
284
285        let mut labeled_comms = Vec::with_capacity(results.len());
286        let mut randomness = Vec::with_capacity(results.len());
287        for result in results {
288            let (comm, rand) = result?;
289            labeled_comms.push(comm);
290            randomness.push(rand);
291        }
292
293        end_timer!(commit_time);
294        Ok((labeled_comms, randomness))
295    }
296
297    pub fn combine_for_open<'a>(
298        universal_prover: &UniversalProver<E>,
299        ck: &CommitterUnionKey<E>,
300        labeled_polynomials: impl ExactSizeIterator<Item = &'a LabeledPolynomial<E::Fr>>,
301        rands: impl ExactSizeIterator<Item = &'a Randomness<E>>,
302        fs_rng: &mut S,
303    ) -> Result<(DensePolynomial<E::Fr>, Randomness<E>)>
304    where
305        Randomness<E>: 'a,
306        Commitment<E>: 'a,
307    {
308        ensure!(labeled_polynomials.len() == rands.len());
309        let mut to_combine = Vec::with_capacity(labeled_polynomials.len());
310
311        for (p, r) in labeled_polynomials.zip_eq(rands) {
312            let enforced_degree_bounds: Option<&[usize]> = ck.enforced_degree_bounds.as_deref();
313
314            kzg10::KZG10::<E>::check_degrees_and_bounds(universal_prover.max_degree, enforced_degree_bounds, p)?;
315            let challenge = fs_rng.squeeze_short_nonnative_field_element::<E::Fr>();
316            to_combine.push((challenge, p.polynomial().to_dense(), r));
317        }
318
319        Ok(Self::combine_polynomials(to_combine))
320    }
321
322    /// On input a list of labeled polynomials and a query set, `open` outputs a
323    /// proof of evaluation of the polynomials at the points in the query
324    /// set.
325    /// `batch_open` over combinations whose polynomials have not been formed.
326    ///
327    /// Both levels of the opening are linear in the coefficients. A combination
328    /// is `Σ c_i · P_i`, and `combine_for_open` then scales that by a
329    /// Fiat-Shamir challenge `d_j` and sums the results. So
330    ///
331    /// ```text
332    ///     Σ_j d_j · ( Σ_i c_ji · P_i )  =  Σ_i ( Σ_j d_j · c_ji ) · P_i
333    /// ```
334    ///
335    /// and composing the two coefficients accumulates the same polynomial in
336    /// **one** pass over each term instead of two. What made the second
337    /// pass easy to miss is that most of Varuna's combinations have a
338    /// single term, so their first pass is a whole scaled copy of a polynomial
339    /// that the second pass then walks again.
340    ///
341    /// This is safe to compose only because the sponge is **squeezed and never
342    /// absorbed** between here and the KZG open, so every challenge is
343    /// determined before any polynomial arithmetic happens. Squeeze order
344    /// is preserved exactly: groups in query-set order, combinations in
345    /// label order within a group, and the trailing squeeze per group.
346    fn open_lazy_combinations(
347        universal_prover: &UniversalProver<E>,
348        ck: &CommitterUnionKey<E>,
349        combinations: &[LazyCombination<'_, E>],
350        query_set: &QuerySet<E::Fr>,
351        fs_rng: &mut S,
352    ) -> Result<BatchProof<E>> {
353        let open_time = start_timer!(|| format!(
354            "Opening {} combinations at query set of size {}",
355            combinations.len(),
356            query_set.len(),
357        ));
358
359        let by_label: BTreeMap<&str, &LazyCombination<'_, E>> =
360            combinations.iter().map(|c| (c.label.as_str(), c)).collect();
361
362        let mut query_to_labels_map = BTreeMap::new();
363        for (label, (point_name, point)) in query_set.iter() {
364            let labels = query_to_labels_map.entry(point_name).or_insert((point, BTreeSet::new()));
365            labels.1.insert(label);
366        }
367
368        let mut proofs = Vec::new();
369        for (_point_name, (&query, labels)) in query_to_labels_map.into_iter() {
370            let mut group = Vec::with_capacity(labels.len());
371            for label in labels {
372                let c = by_label.get(label as &str).ok_or(PCError::MissingPolynomial { label: label.to_string() })?;
373                group.push(*c);
374            }
375
376            let mut challenges = Vec::with_capacity(group.len());
377            for c in &group {
378                // `check_degrees_and_bounds` returns immediately unless the polynomial carries
379                // a degree bound, and a combination only carries one when it
380                // has a single term with coefficient one -- in which case the
381                // combination's polynomial *is* that term's. So checking the
382                // term is the same check on the same coefficients.
383                if let Some(bound) = c.degree_bound {
384                    // Enforced rather than asserted. The invariant is real -- `open_combinations`
385                    // bails unless `lc.len() == 1` before it can set `degree_bound` -- but it is
386                    // established in another function, and `debug_assert!` is compiled out of the
387                    // builds that ship. If it ever broke, nothing would panic: the bound would be
388                    // checked against the first term while the opening covered the whole sum, the
389                    // proof would still verify, and what would quietly be gone is a soundness
390                    // property of the commitment scheme.
391                    ensure!(
392                        c.terms.len() == 1,
393                        "combination {} carries degree bound {bound} with {} terms; \
394                         a degree-bounded combination must have exactly one",
395                        c.label,
396                        c.terms.len(),
397                    );
398                    let enforced_degree_bounds: Option<&[usize]> = ck.enforced_degree_bounds.as_deref();
399                    kzg10::KZG10::<E>::check_degrees_and_bounds(
400                        universal_prover.max_degree,
401                        enforced_degree_bounds,
402                        c.terms[0].1,
403                    )?;
404                }
405                challenges.push(fs_rng.squeeze_short_nonnative_field_element::<E::Fr>());
406            }
407
408            let combine_time = start_timer!(|| format!("Combining {} combinations for the opening", group.len()));
409            let mut polynomial = DensePolynomial::zero();
410            let mut rand = Randomness::empty();
411            for (c, challenge) in group.iter().zip_eq(challenges.iter()) {
412                rand += (*challenge, &c.randomness);
413                for (coeff, p) in &c.terms {
414                    let scale = *challenge * coeff;
415                    if scale.is_one() {
416                        polynomial += p.polynomial();
417                    } else {
418                        polynomial += (scale, p.polynomial());
419                    }
420                }
421            }
422            end_timer!(combine_time);
423
424            let proof_time = start_timer!(|| "Creating proof");
425            let proof = kzg10::KZG10::open(&ck.powers(), &polynomial, query, &rand)?;
426            end_timer!(proof_time);
427            proofs.push(proof);
428
429            let _ = fs_rng.squeeze_short_nonnative_field_element::<E::Fr>();
430        }
431        end_timer!(open_time);
432
433        Ok(BatchProof(proofs))
434    }
435
436    pub fn batch_open<'a>(
437        universal_prover: &UniversalProver<E>,
438        ck: &CommitterUnionKey<E>,
439        labeled_polynomials: impl ExactSizeIterator<Item = &'a LabeledPolynomial<E::Fr>>,
440        query_set: &QuerySet<E::Fr>,
441        rands: impl ExactSizeIterator<Item = &'a Randomness<E>>,
442        fs_rng: &mut S,
443    ) -> Result<BatchProof<E>>
444    where
445        Randomness<E>: 'a,
446        Commitment<E>: 'a,
447    {
448        ensure!(labeled_polynomials.len() == rands.len());
449        let poly_rand: HashMap<_, _> =
450            labeled_polynomials.into_iter().zip_eq(rands).map(|(poly, r)| (poly.label(), (poly, r))).collect();
451
452        let open_time = start_timer!(|| format!(
453            "Opening {} polynomials at query set of size {}",
454            poly_rand.len(),
455            query_set.len(),
456        ));
457
458        let mut query_to_labels_map = BTreeMap::new();
459
460        for (label, (point_name, point)) in query_set.iter() {
461            let labels = query_to_labels_map.entry(point_name).or_insert((point, BTreeSet::new()));
462            labels.1.insert(label);
463        }
464
465        let mut proofs = Vec::new();
466        for (_point_name, (&query, labels)) in query_to_labels_map.into_iter() {
467            let mut query_polys = Vec::with_capacity(labels.len());
468            let mut query_rands = Vec::with_capacity(labels.len());
469
470            for label in labels {
471                let (polynomial, rand) =
472                    poly_rand.get(label as &str).ok_or(PCError::MissingPolynomial { label: label.to_string() })?;
473
474                query_polys.push(*polynomial);
475                query_rands.push(*rand);
476            }
477            let (polynomial, rand) =
478                Self::combine_for_open(universal_prover, ck, query_polys.into_iter(), query_rands.into_iter(), fs_rng)?;
479
480            let proof_time = start_timer!(|| "Creating proof");
481            let proof = kzg10::KZG10::open(&ck.powers(), &polynomial, query, &rand)?;
482            end_timer!(proof_time);
483            proofs.push(proof);
484
485            let _ = fs_rng.squeeze_short_nonnative_field_element::<E::Fr>();
486        }
487        let batch_proof = BatchProof(proofs);
488        end_timer!(open_time);
489
490        Ok(batch_proof)
491    }
492
493    pub fn batch_check<'a>(
494        vk: &UniversalVerifier<E>,
495        commitments: impl IntoIterator<Item = &'a LabeledCommitment<Commitment<E>>>,
496        query_set: &QuerySet<E::Fr>,
497        values: &Evaluations<E::Fr>,
498        proof: &BatchProof<E>,
499        fs_rng: &mut S,
500    ) -> Result<bool>
501    where
502        Commitment<E>: 'a,
503    {
504        let commitments: BTreeMap<_, _> = commitments.into_iter().map(|c| (c.label().to_owned(), c)).collect();
505        let batch_check_time = start_timer!(|| format!(
506            "Checking {} commitments at query set of size {}",
507            commitments.len(),
508            query_set.len(),
509        ));
510        let mut query_to_labels_map = BTreeMap::new();
511
512        for (label, (point_name, point)) in query_set.iter() {
513            let labels = query_to_labels_map.entry(point_name).or_insert((point, BTreeSet::new()));
514            labels.1.insert(label);
515        }
516
517        ensure!(query_to_labels_map.len() == proof.0.len());
518
519        let mut private_sponge = fs_rng.clone();
520        for proof in &proof.0 {
521            proof.absorb_into_sponge(&mut private_sponge);
522        }
523
524        let mut randomizer = E::Fr::one();
525
526        let mut combined_comms = BTreeMap::new();
527        let mut combined_witness = E::G1Projective::zero();
528        let mut combined_adjusted_witness = E::G1Projective::zero();
529
530        for ((_query_name, (query, labels)), p) in query_to_labels_map.into_iter().zip_eq(&proof.0) {
531            let mut comms_to_combine: Vec<&'_ LabeledCommitment<_>> = Vec::new();
532            let mut values_to_combine = Vec::new();
533            for label in labels.into_iter() {
534                let commitment =
535                    commitments.get(label).ok_or(PCError::MissingPolynomial { label: label.to_string() })?;
536
537                let v_i = values
538                    .get(&(label.clone(), *query))
539                    .ok_or(PCError::MissingEvaluation { label: label.to_string() })?;
540
541                comms_to_combine.push(commitment);
542                values_to_combine.push(*v_i);
543            }
544
545            Self::accumulate_elems(
546                &mut combined_comms,
547                &mut combined_witness,
548                &mut combined_adjusted_witness,
549                vk,
550                comms_to_combine.into_iter(),
551                *query,
552                values_to_combine.into_iter(),
553                p,
554                Some(randomizer),
555                fs_rng,
556            )?;
557
558            let _ = fs_rng.squeeze_short_nonnative_field_element::<E::Fr>();
559
560            randomizer = private_sponge.squeeze_short_nonnative_field_element::<E::Fr>();
561        }
562
563        let result = Self::check_elems(vk, combined_comms, combined_witness, combined_adjusted_witness);
564        end_timer!(batch_check_time);
565        result
566    }
567
568    pub fn open_combinations<'a>(
569        universal_prover: &UniversalProver<E>,
570        ck: &CommitterUnionKey<E>,
571        linear_combinations: impl IntoIterator<Item = &'a LinearCombination<E::Fr>>,
572        polynomials: impl IntoIterator<Item = LabeledPolynomial<E::Fr>>,
573        rands: impl IntoIterator<Item = &'a Randomness<E>>,
574        query_set: &QuerySet<E::Fr>,
575        fs_rng: &mut S,
576    ) -> Result<BatchLCProof<E>>
577    where
578        Randomness<E>: 'a,
579        Commitment<E>: 'a,
580    {
581        let label_map =
582            polynomials.into_iter().zip_eq(rands).map(|(p, r)| (p.to_label(), (p, r))).collect::<BTreeMap<_, _>>();
583
584        // Resolve each combination to its terms without forming its polynomial.
585        //
586        // The arithmetic below is over scalars: `lc` is symbolic, its terms naming
587        // polynomials rather than holding them. Forming the polynomial here is
588        // exactly what `open_lazy_combinations` avoids.
589        let resolve_time = start_timer!(|| "Resolving combination terms");
590        let mut combinations = Vec::new();
591        for lc in linear_combinations {
592            let lc_label = lc.label().to_string();
593            // `lc.len()` counts terms *before* the `is_one()` filter below, so this
594            // over-reserves by the number of constant terms. Left as is because
595            // that pre-filter reading is what the degree-bound guard relies on.
596            let mut terms = Vec::with_capacity(lc.len());
597            let mut randomness = Randomness::empty();
598            let mut degree_bound = None;
599
600            let num_polys = lc.len();
601            // We filter out l.is_one() entries because those constants are not committed to
602            // and used directly by the verifier.
603            for (coeff, label) in lc.iter().filter(|(_, l)| !l.is_one()) {
604                let label: &String = label.try_into().expect("cannot be one!");
605                let (cur_poly, cur_rand) =
606                    label_map.get(label as &str).ok_or(PCError::MissingPolynomial { label: label.to_string() })?;
607                if let Some(cur_degree_bound) = cur_poly.degree_bound() {
608                    if num_polys != 1 {
609                        bail!(PCError::EquationHasDegreeBounds(lc_label));
610                    }
611                    assert!(coeff.is_one(), "Coefficient must be one for degree-bounded equations");
612                    if let Some(old_degree_bound) = degree_bound {
613                        assert_eq!(old_degree_bound, cur_degree_bound)
614                    } else {
615                        degree_bound = cur_poly.degree_bound();
616                    }
617                }
618                terms.push((*coeff, cur_poly));
619                randomness += (*coeff, *cur_rand);
620            }
621
622            // The hiding bound is not carried. It was only ever handed to the
623            // `LabeledPolynomial` built here, and nothing downstream of the
624            // opening reads it: the hiding bound is enforced when the
625            // polynomial is committed, not when it is opened.
626            combinations.push(LazyCombination { label: lc_label, terms, degree_bound, randomness });
627        }
628        end_timer!(resolve_time);
629
630        let proof = Self::open_lazy_combinations(universal_prover, ck, &combinations, query_set, fs_rng)?;
631
632        Ok(BatchLCProof { proof })
633    }
634
635    /// Checks that `values` are the true evaluations at `query_set` of the
636    /// polynomials committed in `labeled_commitments`.
637    pub fn check_combinations<'a>(
638        vk: &UniversalVerifier<E>,
639        linear_combinations: impl IntoIterator<Item = &'a LinearCombination<E::Fr>>,
640        commitments: impl IntoIterator<Item = &'a LabeledCommitment<Commitment<E>>>,
641        query_set: &QuerySet<E::Fr>,
642        evaluations: &Evaluations<E::Fr>,
643        proof: &BatchLCProof<E>,
644        fs_rng: &mut S,
645    ) -> Result<bool>
646    where
647        Commitment<E>: 'a,
648    {
649        let BatchLCProof { proof } = proof;
650        let label_comm_map = commitments.into_iter().map(|c| (c.label(), c)).collect::<BTreeMap<_, _>>();
651
652        let mut lc_commitments = Vec::new();
653        let mut lc_info = Vec::new();
654        let mut evaluations = evaluations.clone();
655
656        let lc_processing_time = start_timer!(|| "Combining commitments");
657        for lc in linear_combinations {
658            let lc_label = lc.label().to_string();
659            let num_polys = lc.len();
660
661            let mut degree_bound = None;
662            let mut coeffs_and_comms = Vec::new();
663
664            for (coeff, label) in lc.iter() {
665                if label.is_one() {
666                    for ((label, _), ref mut eval) in evaluations.iter_mut() {
667                        if label == &lc_label {
668                            **eval -= coeff;
669                        }
670                    }
671                } else {
672                    let label: &String = label.try_into().unwrap();
673                    let &cur_comm = label_comm_map
674                        .get(label as &str)
675                        .ok_or(PCError::MissingPolynomial { label: label.to_string() })?;
676
677                    if cur_comm.degree_bound().is_some() {
678                        if num_polys != 1 || !coeff.is_one() {
679                            bail!(PCError::EquationHasDegreeBounds(lc_label));
680                        }
681                        degree_bound = cur_comm.degree_bound();
682                    }
683                    coeffs_and_comms.push((*coeff, cur_comm.commitment()));
684                }
685            }
686            let lc_time = start_timer!(|| format!("Combining {num_polys} commitments for {lc_label}"));
687            lc_commitments.push(Self::combine_commitments(coeffs_and_comms));
688            end_timer!(lc_time);
689            lc_info.push((lc_label, degree_bound));
690        }
691        end_timer!(lc_processing_time);
692
693        let combined_comms_norm_time = start_timer!(|| "Normalizing commitments");
694        let comms = Self::normalize_commitments(lc_commitments);
695        ensure!(lc_info.len() == comms.len());
696        let lc_commitments = lc_info
697            .into_iter()
698            .zip_eq(comms)
699            .map(|((label, d), c)| LabeledCommitment::new(label, c, d))
700            .collect::<Vec<_>>();
701        end_timer!(combined_comms_norm_time);
702
703        Self::batch_check(vk, &lc_commitments, query_set, &evaluations, proof, fs_rng)
704    }
705}
706
707impl<E: PairingEngine, S: AlgebraicSponge<E::Fq, 2>> SonicKZG10<E, S> {
708    fn combine_polynomials<'a, B: Borrow<DensePolynomial<E::Fr>>>(
709        coeffs_polys_rands: impl IntoIterator<Item = (E::Fr, B, &'a Randomness<E>)>,
710    ) -> (DensePolynomial<E::Fr>, Randomness<E>) {
711        let mut combined_poly = DensePolynomial::zero();
712        let mut combined_rand = Randomness::empty();
713        for (coeff, poly, rand) in coeffs_polys_rands {
714            let poly = poly.borrow();
715            if coeff.is_one() {
716                combined_poly += poly;
717                combined_rand += rand;
718            } else {
719                combined_poly += (coeff, poly);
720                combined_rand += (coeff, rand);
721            }
722        }
723        (combined_poly, combined_rand)
724    }
725
726    /// MSM for `commitments` and `coeffs`
727    fn combine_commitments<'a>(
728        coeffs_and_comms: impl IntoIterator<Item = (E::Fr, &'a Commitment<E>)>,
729    ) -> E::G1Projective {
730        let (scalars, bases): (Vec<_>, Vec<_>) = coeffs_and_comms.into_iter().map(|(f, c)| (f.into(), c.0)).unzip();
731        VariableBase::msm(&bases, &scalars)
732    }
733
734    fn normalize_commitments(commitments: Vec<E::G1Projective>) -> impl ExactSizeIterator<Item = Commitment<E>> {
735        let comms = E::G1Projective::batch_normalization_into_affine(commitments);
736        comms.into_iter().map(|c| kzg10::KZGCommitment(c))
737    }
738}
739
740impl<E: PairingEngine, S: AlgebraicSponge<E::Fq, 2>> SonicKZG10<E, S> {
741    #[allow(clippy::too_many_arguments)]
742    fn accumulate_elems<'a>(
743        combined_comms: &mut BTreeMap<Option<usize>, E::G1Projective>,
744        combined_witness: &mut E::G1Projective,
745        combined_adjusted_witness: &mut E::G1Projective,
746        vk: &UniversalVerifier<E>,
747        commitments: impl ExactSizeIterator<Item = &'a LabeledCommitment<Commitment<E>>>,
748        point: E::Fr,
749        values: impl ExactSizeIterator<Item = E::Fr>,
750        proof: &kzg10::KZGProof<E>,
751        randomizer: Option<E::Fr>,
752        fs_rng: &mut S,
753    ) -> Result<()> {
754        let acc_time = start_timer!(|| "Accumulating elements");
755        // Keeps track of running combination of values
756        let mut combined_values = E::Fr::zero();
757
758        // Iterates through all of the commitments and accumulates common degree_bound
759        // elements in a BTreeMap
760        ensure!(commitments.len() == values.len());
761        for (labeled_comm, value) in commitments.into_iter().zip_eq(values) {
762            let acc_timer = start_timer!(|| format!("Accumulating {}", labeled_comm.label()));
763            let curr_challenge = fs_rng.squeeze_short_nonnative_field_element::<E::Fr>();
764
765            combined_values += &(value * curr_challenge);
766
767            let comm = labeled_comm.commitment();
768            let degree_bound = labeled_comm.degree_bound();
769
770            // Applying opening challenge and randomness (used in batch_checking)
771            let coeff = randomizer.unwrap_or_else(E::Fr::one) * curr_challenge;
772            let comm_with_challenge: E::G1Projective = comm.0.mul(coeff);
773
774            // Accumulate values in the BTreeMap
775            *combined_comms.entry(degree_bound).or_insert_with(E::G1Projective::zero) += &comm_with_challenge;
776            end_timer!(acc_timer);
777        }
778
779        // Push expected results into list of elems. Power will be the negative of the
780        // expected power
781        let mut bases = vec![vk.vk.g, -proof.w];
782        let mut coeffs = vec![combined_values, point];
783        if let Some(random_v) = proof.random_v {
784            bases.push(vk.vk.gamma_g);
785            coeffs.push(random_v);
786        }
787        *combined_witness += if let Some(randomizer) = randomizer {
788            coeffs.iter_mut().for_each(|c| *c *= randomizer);
789            proof.w.mul(randomizer)
790        } else {
791            proof.w.to_projective()
792        };
793        let coeffs = coeffs.into_iter().map(|c| c.into()).collect::<Vec<_>>();
794        *combined_adjusted_witness += VariableBase::msm(&bases, &coeffs);
795        end_timer!(acc_time);
796        Ok(())
797    }
798
799    fn check_elems(
800        vk: &UniversalVerifier<E>,
801        combined_comms: BTreeMap<Option<usize>, E::G1Projective>,
802        combined_witness: E::G1Projective,
803        combined_adjusted_witness: E::G1Projective,
804    ) -> Result<bool> {
805        let check_time = start_timer!(|| "Checking elems");
806        let mut g1_projective_elems = Vec::with_capacity(combined_comms.len() + 2);
807        let mut g2_prepared_elems = Vec::with_capacity(combined_comms.len() + 2);
808
809        for (degree_bound, comm) in combined_comms.into_iter() {
810            let shift_power = if let Some(degree_bound) = degree_bound {
811                // Find the appropriate prepared shift for the degree bound.
812                vk.prepared_negative_powers_of_beta_h
813                    .get(&degree_bound)
814                    .cloned()
815                    .ok_or(PCError::UnsupportedDegreeBound(degree_bound))?
816            } else {
817                vk.vk.prepared_h.clone()
818            };
819
820            g1_projective_elems.push(comm);
821            g2_prepared_elems.push(shift_power);
822        }
823
824        g1_projective_elems.push(-combined_adjusted_witness);
825        g2_prepared_elems.push(vk.vk.prepared_h.clone());
826
827        g1_projective_elems.push(-combined_witness);
828        g2_prepared_elems.push(vk.vk.prepared_beta_h.clone());
829
830        let g1_prepared_elems_iter = E::G1Projective::batch_normalization_into_affine(g1_projective_elems)
831            .into_iter()
832            .map(|a| a.prepare())
833            .collect::<Vec<_>>();
834
835        ensure!(g1_prepared_elems_iter.len() == g2_prepared_elems.len());
836        let g1_g2_prepared = g1_prepared_elems_iter.iter().zip_eq(g2_prepared_elems.iter());
837        let is_one: bool = E::product_of_pairings(g1_g2_prepared).is_one();
838        end_timer!(check_time);
839        Ok(is_one)
840    }
841}
842
843#[cfg(test)]
844mod tests {
845    #![allow(non_camel_case_types)]
846
847    use super::{Bases, CommitterKey, CommitterUnionKey, SonicKZG10};
848    use crate::{crypto_hash::PoseidonSponge, polycommit::test_templates::*};
849    use snarkvm_curves::bls12_377::{Bls12_377, Fq};
850    use snarkvm_utilities::{FromBytes, ToBytes, rand::TestRng};
851
852    use itertools::Itertools;
853    use rand::distr::Distribution;
854
855    type Sponge = PoseidonSponge<Fq, 2, 1>;
856    type PC_Bls12_377 = SonicKZG10<Bls12_377, Sponge>;
857
858    #[test]
859    fn test_committer_key_serialization() {
860        let rng = &mut TestRng::default();
861        let max_degree = rand::distr::Uniform::new_inclusive(8, 64).unwrap().sample(rng);
862        let supported_degree = rand::distr::Uniform::new_inclusive(1, max_degree).unwrap().sample(rng);
863
864        let lagrange_size = |d: usize| if d.is_power_of_two() { d } else { d.next_power_of_two() >> 1 };
865
866        let pp = PC_Bls12_377::load_srs(max_degree).unwrap();
867
868        let (ck, _vk) = PC_Bls12_377::trim(&pp, supported_degree, [lagrange_size(supported_degree)], 0, None).unwrap();
869
870        let ck_bytes = ck.to_bytes_le().unwrap();
871        let ck_recovered: CommitterKey<Bls12_377> = FromBytes::read_le(&ck_bytes[..]).unwrap();
872        let ck_recovered_bytes = ck_recovered.to_bytes_le().unwrap();
873
874        assert_eq!(&ck_bytes, &ck_recovered_bytes);
875    }
876
877    /// `trim` shares its bases with the SRS instead of copying them, and they
878    /// are the same points a copy held.
879    #[test]
880    fn trim_shares_its_bases_with_the_srs() {
881        let pp = PC_Bls12_377::load_srs((1 << 12) - 1).unwrap();
882        let (ck, _) = PC_Bls12_377::trim(&pp, 1000, [], 1, Some(&[500])).unwrap();
883        let top = pp.max_degree();
884
885        assert!(matches!(ck.powers_of_beta_g, Bases::Shared { .. }));
886        assert_eq!(ck.powers_of_beta_g.owned_capacity(), 0, "a shared prefix owns nothing");
887        assert_eq!(&*ck.powers_of_beta_g, pp.powers_of_beta_g(0, 1001).unwrap().as_slice());
888
889        let shifted = ck.shifted_powers_of_beta_g.as_ref().unwrap();
890        assert!(matches!(shifted, Bases::Shared { .. }));
891        assert_eq!(shifted.owned_capacity(), 0, "a shared suffix owns nothing");
892        assert_eq!(&**shifted, pp.powers_of_beta_g(top - 500, top + 1).unwrap().as_slice());
893
894        // Bytes, and so the key's hash, are those of the points, however held.
895        let bytes = ck.to_bytes_le().unwrap();
896        let read: CommitterKey<Bls12_377> = FromBytes::read_le(&bytes[..]).unwrap();
897        assert!(matches!(read.powers_of_beta_g, Bases::Owned(_)), "a key read from bytes owns its bases");
898        assert_eq!(read.to_bytes_le().unwrap(), bytes);
899    }
900
901    /// Growing the SRS replaces its snapshot, never mutating the one a key
902    /// shares: a key trimmed before the growth reads the same points after it.
903    #[test]
904    fn a_key_keeps_its_snapshot_across_a_growth() {
905        let pp = PC_Bls12_377::load_srs((1 << 15) - 1).unwrap();
906        let (before, _) = PC_Bls12_377::trim(&pp, 1000, [], 1, Some(&[500])).unwrap();
907        let points = before.powers_of_beta_g.to_vec();
908
909        pp.download_powers_for(0..(1 << 16)).unwrap();
910        let (after, _) = PC_Bls12_377::trim(&pp, 1000, [], 1, Some(&[500])).unwrap();
911
912        assert_eq!(&*before.powers_of_beta_g, points.as_slice(), "the older snapshot changed");
913        assert_eq!(&*after.powers_of_beta_g, points.as_slice());
914        assert_ne!(
915            before.powers_of_beta_g.as_ptr(),
916            after.powers_of_beta_g.as_ptr(),
917            "a key trimmed after the growth should share the new snapshot"
918        );
919    }
920
921    /// A `trim` whose Lagrange sizes grow the SRS shares the snapshot that
922    /// growth made, not the one it replaced.
923    #[test]
924    fn trim_shares_the_snapshot_left_after_its_own_downloads() {
925        let pp = PC_Bls12_377::load_srs((1 << 15) - 1).unwrap();
926        let (ck, _) = PC_Bls12_377::trim(&pp, 1000, [1 << 16], 1, Some(&[500])).unwrap();
927        let (current, _) = pp.shared_powers_of_beta_g(0, 1).unwrap();
928        match &ck.powers_of_beta_g {
929            Bases::Shared { store, .. } => {
930                assert!(std::sync::Arc::ptr_eq(store, &current), "the key shares a snapshot its own trim replaced")
931            }
932            Bases::Owned(_) => panic!("trim should share"),
933        }
934    }
935
936    /// An empty range asks for nothing, so it neither downloads nor replaces
937    /// the snapshot keys share, even one starting where the held prefix ends.
938    #[test]
939    fn an_empty_range_leaves_the_snapshot_alone() {
940        let pp = PC_Bls12_377::load_srs((1 << 15) - 1).unwrap();
941        let (before, _) = pp.shared_powers_of_beta_g(0, 1).unwrap();
942        let held = 1 << 15;
943        assert!(pp.powers_of_beta_g(held, held).unwrap().is_empty());
944        assert!(pp.powers_of_beta_g(0, 0).unwrap().is_empty());
945        let (after, _) = pp.shared_powers_of_beta_g(0, 1).unwrap();
946        assert!(std::sync::Arc::ptr_eq(&before, &after), "an empty range replaced the snapshot");
947    }
948
949    /// A `trim` that fails validation downloads nothing first, so a rejected
950    /// call leaves the SRS, snapshot included, as it was.
951    #[test]
952    fn a_rejected_trim_downloads_nothing() {
953        let pp = PC_Bls12_377::load_srs((1 << 15) - 1).unwrap();
954        let (before, _) = pp.shared_powers_of_beta_g(0, 1).unwrap();
955        let grown = 1 << 16;
956        // A degree bound above the supported degree.
957        assert!(PC_Bls12_377::trim(&pp, grown - 1, [], 1, Some(&[grown])).is_err());
958        // A Lagrange size that is not a power of two, and one beyond the SRS.
959        assert!(PC_Bls12_377::trim(&pp, grown - 1, [3], 1, None).is_err());
960        assert!(PC_Bls12_377::trim(&pp, grown - 1, [1 << 29], 1, None).is_err());
961        let (after, _) = pp.shared_powers_of_beta_g(0, 1).unwrap();
962        assert!(std::sync::Arc::ptr_eq(&before, &after), "a rejected trim grew the SRS");
963    }
964
965    /// `to_mut` turns a shared range into an owned copy, leaving the SRS as it
966    /// was.
967    #[test]
968    fn editing_a_shared_key_copies_it_first() {
969        use super::Bases;
970        let pp = PC_Bls12_377::load_srs((1 << 12) - 1).unwrap();
971        let (mut ck, _) = PC_Bls12_377::trim(&pp, 1000, [], 1, Some(&[500])).unwrap();
972        ck.powers_of_beta_g.to_mut().swap(0, 1);
973        assert!(matches!(ck.powers_of_beta_g, Bases::Owned(_)));
974        assert_ne!(&*ck.powers_of_beta_g, pp.powers_of_beta_g(0, 1001).unwrap().as_slice());
975        let (fresh, _) = PC_Bls12_377::trim(&pp, 1000, [], 1, Some(&[500])).unwrap();
976        assert_eq!(&*fresh.powers_of_beta_g, pp.powers_of_beta_g(0, 1001).unwrap().as_slice());
977    }
978
979    #[test]
980    fn test_union_takes_each_array_from_the_key_where_it_is_longest() {
981        let pp = PC_Bls12_377::load_srs(32).unwrap();
982        // Each key holds the longest of a different array: the prefix, the shifted
983        // powers, and the hiding powers, including for the bound all three enforce.
984        let (long_prefix, _) = PC_Bls12_377::trim(&pp, 16, [], 0, Some(&[4])).unwrap();
985        let (long_suffix, _) = PC_Bls12_377::trim(&pp, 12, [], 0, Some(&[4, 10])).unwrap();
986        let (long_hiding, _) = PC_Bls12_377::trim(&pp, 8, [], 1, Some(&[4])).unwrap();
987        let shifted_hiding = |ck: &CommitterKey<Bls12_377>, bound| {
988            ck.shifted_powers_of_beta_times_gamma_g.as_ref().unwrap()[&bound].clone()
989        };
990
991        for keys in [&long_prefix, &long_suffix, &long_hiding].into_iter().permutations(3) {
992            let union = CommitterUnionKey::union(keys);
993            assert_eq!(&*union.powers().powers_of_beta_g, &*long_prefix.powers_of_beta_g);
994            assert_eq!(union.powers().powers_of_beta_times_gamma_g, long_hiding.powers_of_beta_times_gamma_g);
995            for (bound, hiding) in [(4, shifted_hiding(&long_hiding, 4)), (10, shifted_hiding(&long_suffix, 10))] {
996                let shifted = union.shifted_powers_of_beta_g(bound).unwrap();
997                let top = pp.max_degree();
998                assert_eq!(
999                    shifted.powers_of_beta_g,
1000                    pp.powers_of_beta_g(top - bound, top + 1).unwrap(),
1001                    "bound {bound}"
1002                );
1003                assert_eq!(shifted.powers_of_beta_times_gamma_g, hiding, "bound {bound}");
1004            }
1005        }
1006    }
1007
1008    #[test]
1009    fn test_single_poly() {
1010        single_poly_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1011    }
1012
1013    #[test]
1014    fn test_quadratic_poly_degree_bound_multiple_queries() {
1015        quadratic_poly_degree_bound_multiple_queries_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1016    }
1017
1018    #[test]
1019    fn test_linear_poly_degree_bound() {
1020        linear_poly_degree_bound_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1021    }
1022
1023    #[test]
1024    fn test_single_poly_degree_bound() {
1025        single_poly_degree_bound_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1026    }
1027
1028    #[test]
1029    fn test_single_poly_degree_bound_multiple_queries() {
1030        single_poly_degree_bound_multiple_queries_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1031    }
1032
1033    #[test]
1034    fn test_two_polys_degree_bound_single_query() {
1035        two_polys_degree_bound_single_query_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1036    }
1037
1038    #[test]
1039    fn test_full_end_to_end() {
1040        full_end_to_end_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1041        println!("Finished bls12-377");
1042    }
1043
1044    #[test]
1045    fn test_single_equation() {
1046        single_equation_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1047        println!("Finished bls12-377");
1048    }
1049
1050    #[test]
1051    fn test_two_equation() {
1052        two_equation_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1053        println!("Finished bls12-377");
1054    }
1055
1056    #[test]
1057    fn test_two_equation_degree_bound() {
1058        two_equation_degree_bound_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1059        println!("Finished bls12-377");
1060    }
1061
1062    #[test]
1063    fn test_full_end_to_end_equation() {
1064        full_end_to_end_equation_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1065        println!("Finished bls12-377");
1066    }
1067
1068    #[test]
1069    #[should_panic]
1070    fn test_bad_degree_bound() {
1071        bad_degree_bound_test::<Bls12_377, Sponge>().expect("test failed for bls12-377");
1072        println!("Finished bls12-377");
1073    }
1074
1075    #[test]
1076    fn test_lagrange_commitment() {
1077        crate::polycommit::test_templates::lagrange_test_template::<Bls12_377, Sponge>()
1078            .expect("test failed for bls12-377");
1079        println!("Finished bls12-377");
1080    }
1081}