Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement batch operations in non_hiding_kzg module #269

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions src/provider/non_hiding_kzg.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
//! Non-hiding variant of KZG10 scheme for univariate polynomials.
use crate::zip_with_for_each;
use abomonation_derive::Abomonation;
use ff::{Field, PrimeField, PrimeFieldBits};
use group::{prime::PrimeCurveAffine, Curve, Group as _};
use itertools::Itertools as _;
use pairing::{Engine, MillerLoopResult, MultiMillerLoop};
use rand_core::{CryptoRng, RngCore};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::{borrow::Borrow, marker::PhantomData, ops::Mul};

Expand Down Expand Up @@ -278,6 +281,20 @@ where
Ok(UVKZGCommitment(C.to_affine()))
}

/// Generate a commitment for a list of polynomials
#[allow(dead_code)]
pub fn batch_commit(
prover_param: impl Borrow<KZGProverKey<E>>,
polys: &[UVKZGPoly<E::Fr>],
) -> Result<Vec<UVKZGCommitment<E>>, NovaError> {
let prover_param = prover_param.borrow();

polys
.into_par_iter()
.map(|poly| Self::commit(prover_param, poly))
.collect::<Result<Vec<UVKZGCommitment<E>>, NovaError>>()
}

/// On input a polynomial `p` and a point `point`, outputs a proof for the
/// same.
pub fn open(
Expand Down Expand Up @@ -307,6 +324,31 @@ where
))
}

/// Input a list of polynomials, and a same number of points,
/// compute a multi-opening for all the polynomials.
// This is a naive approach
// TODO: to implement a more efficient batch opening algorithm
#[allow(dead_code)]
pub fn batch_open(
prover_param: impl Borrow<KZGProverKey<E>>,
polynomials: &[UVKZGPoly<E::Fr>],
points: &[E::Fr],
) -> Result<(Vec<UVKZGProof<E>>, Vec<UVKZGEvaluation<E>>), NovaError> {
if polynomials.len() != points.len() {
// TODO: a better Error
return Err(NovaError::PCSError(PCSError::LengthError));
}
let mut batch_proof = vec![];
let mut evals = vec![];
for (poly, point) in polynomials.iter().zip_eq(points.iter()) {
let (proof, eval) = Self::open(prover_param.borrow(), poly, point)?;
batch_proof.push(proof);
evals.push(eval);
}

Ok((batch_proof, evals))
}

/// Verifies that `value` is the evaluation at `x` of the polynomial
/// committed inside `comm`.
#[allow(dead_code, clippy::unnecessary_wraps)]
Expand Down Expand Up @@ -334,6 +376,61 @@ where
let pairing_result = E::multi_miller_loop(pairing_input_refs.as_slice()).final_exponentiation();
Ok(pairing_result.is_identity().into())
}

/// Verifies that `value_i` is the evaluation at `x_i` of the polynomial
/// `poly_i` committed inside `comm`.
// This is a naive approach
// TODO: to implement the more efficient batch verification algorithm
#[allow(dead_code, clippy::unnecessary_wraps)]
pub fn batch_verify<R: RngCore + CryptoRng>(
verifier_params: impl Borrow<KZGVerifierKey<E>>,
multi_commitment: &[UVKZGCommitment<E>],
points: &[E::Fr],
values: &[UVKZGEvaluation<E>],
batch_proof: &[UVKZGProof<E>],
rng: &mut R,
) -> Result<bool, NovaError> {
let verifier_params = verifier_params.borrow();

let mut total_c = <E::G1>::identity();
let mut total_w = <E::G1>::identity();

let mut randomizer = E::Fr::ONE;
// Instead of multiplying g and gamma_g in each turn, we simply accumulate
// their coefficients and perform a final multiplication at the end.
let mut g_multiplier = E::Fr::ZERO;
zip_with_for_each!(
into_iter,
(multi_commitment, points, values, batch_proof),
|c, z, v, proof| {
let w = proof.proof;
let mut temp = w.mul(*z);
temp += &c.0;
let c = temp;
g_multiplier += &(randomizer * v.0);
total_c += &c.mul(randomizer);
total_w += &w.mul(randomizer);
// We don't need to sample randomizers from the full field,
// only from 128-bit strings.
randomizer = E::Fr::from_u128(rand::Rng::gen::<u128>(rng));
}
);
total_c -= &verifier_params.g.mul(g_multiplier);

let mut affine_points = vec![E::G1Affine::identity(); 2];
E::G1::batch_normalize(&[-total_w, total_c], &mut affine_points);
let (total_w, total_c) = (affine_points[0], affine_points[1]);

let result = E::multi_miller_loop(&[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Adrian mentioned (and I then could also convince myself by observing the implementation), this form of pairings check is more optimal, comparing to:

let left = E::pairing(..., ...);
let right = E::pairing(..., ...);
assert_eq!(left, right);

as it saves one multi_miller_loop and one final_exponentiation call. However that requires some manipulating with input parameters, which differs from what is necessary to pass to pairing couple. For example, given vanilla KZG10 specification, described here, the final check is specified like:

e(C_q, a⋅G2−b⋅G2) == e(C_f−G1⋅c, G2) 

which has following Rust equivalent:

let g1 = srs.powers_of_g[0];
let g2 = srs.powers_of_h[0];

let left = pairing(
  &Cq.to_affine(),
  &(srs.powers_of_h[1] - (g2 * b)).to_affine(),
);
let right = pairing(&(Cf.to_affine() - (g1 * c).to_affine()).to_affine(), &g2);
assert_eq!(left, right);

Alternatively (more optimally), one can write this check as:

// Alternatively (more optimal as it doesn't require executing final_exponentiation twice)
let term1 = (
  &(srs.powers_of_g[0] * c - Cq * b - Cf).to_affine(),
  &G2Prepared::from_affine(srs.powers_of_h[0]),
);
let term2 = (
  &Cq.to_affine(),
  &G2Prepared::from_affine(srs.powers_of_h[1]),
);
let result = multi_miller_loop(&[term1, term2]).final_exponentiation();
assert_eq!(result.is_identity().unwrap_u8(), 0x01);

which I derived empirically.

I wonder if there is a systematic way that allows turning the e() == e() formula into more optimal one:

term1 = ...
term2 = ...
multi_miller_loop(term1, term2).final_exponentiation().is_identity()

Thanks in advance, for answering, @huitseeker !

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the underpinnings of the speed of this API have been discussed elsewhere, but to summarize:

  1. the vectorized API allows us to perform less (a single) final exponentiation, which is an expensive operation,
  2. the initial part of the pairing computation, the Miller loop, is a fancy double-and-add algorithm that's easy(-ish) to mutualize in an SIMD fashion, giving the vectorized API a bit of an edge,

Hence the API you're remarking on above. The equivalent API in Arkworks is perhaps a bit simpler, but leads to the same idea:
https://github.com/arkworks-rs/algebra/blob/bf96a5b2873e69f3c378c7b25d0901a6701efcc4/ec/src/pairing.rs#L104-L109

(&total_w, &verifier_params.beta_h.into()),
(&total_c, &verifier_params.h.into()),
])
.final_exponentiation()
.is_identity()
.into();

Ok(result)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -378,4 +475,47 @@ mod tests {
fn end_to_end_test() {
end_to_end_test_template::<halo2curves::bn256::Bn256>().expect("test failed for Bn256");
}

fn batch_check_test_template<E>() -> Result<(), NovaError>
where
E: MultiMillerLoop,
E::Fr: PrimeFieldBits,
E::G1: DlogGroup<ScalarExt = E::Fr, AffineExt = E::G1Affine>,
{
for _ in 0..10 {
let mut rng = &mut thread_rng();

let degree = rng.gen_range(2..20);

let pp = UniversalKZGParam::<E>::gen_srs_for_testing(&mut rng, degree);
let (ck, vk) = pp.trim(degree);

let mut comms = Vec::new();
let mut values = Vec::new();
let mut points = Vec::new();
let mut proofs = Vec::new();
for _ in 0..10 {
let mut rng = rng.clone();
let p = random(degree, &mut rng);
let comm = UVKZGPCS::<E>::commit(&ck, &p)?;
let point = E::Fr::random(rng);
let (proof, value) = UVKZGPCS::<E>::open(&ck, &p, &point)?;

assert!(UVKZGPCS::<E>::verify(&vk, &comm, &point, &proof, &value)?);
comms.push(comm);
values.push(value);
points.push(point);
proofs.push(proof);
}
assert!(UVKZGPCS::<E>::batch_verify(
&vk, &comms, &points, &values, &proofs, &mut rng
)?);
}
Ok(())
}

#[test]
fn batch_check_test() {
batch_check_test_template::<halo2curves::bn256::Bn256>().expect("test failed for Bn256");
}
}