diff --git a/Cargo.toml b/Cargo.toml index c2d12c0fb..99536b319 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ thiserror = "1.0" group = "0.13.0" once_cell = "1.18.0" itertools = "0.12.0" +rand = "0.8.5" +ref-cast = "1.0.20" [target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] pasta-msm = { version = "0.1.4" } diff --git a/src/errors.rs b/src/errors.rs index e2ff3d02f..8ad023fad 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -33,9 +33,9 @@ pub enum NovaError { /// returned if the provided number of steps is zero #[error("InvalidNumSteps")] InvalidNumSteps, - /// returned when an invalid inner product argument is provided - #[error("InvalidIPA")] - InvalidIPA, + /// returned if there is an error in the proof/verification of a PCS + #[error("PCSError")] + PCSError(#[from] PCSError), /// returned when an invalid sum-check proof is provided #[error("InvalidSumcheckProof")] InvalidSumcheckProof, @@ -78,3 +78,17 @@ impl From for NovaError { } } } + +/// Errors specific to the Polynomial commitment scheme +#[derive(Clone, Debug, Eq, PartialEq, Error)] +pub enum PCSError { + /// returned when an invalid inner product argument is provided + #[error("InvalidIPA")] + InvalidIPA, + /// returned when there is a Zeromorph error + #[error("ZMError")] + ZMError, + /// returned when a length check fails in a PCS + #[error("LengthError")] + LengthError, +} diff --git a/src/lib.rs b/src/lib.rs index f35b80748..c62cb51bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -846,8 +846,8 @@ mod tests { use super::*; use crate::{ provider::{ - hyperkzg::Bn256EngineKZG, pedersen::CommitmentKeyExtTrait, traits::DlogGroup, Bn256Engine, - GrumpkinEngine, PallasEngine, Secp256k1Engine, Secq256k1Engine, VestaEngine, + hyperkzg::Bn256EngineKZG, traits::DlogGroup, Bn256Engine, Bn256EngineZM, + GrumpkinEngine, PallasEngine, Secp256k1Engine, Secq256k1Engine, VestaEngine, non_hiding_zeromorph::ZMPCS, }, traits::{circuit::TrivialCircuit, evaluation::EvaluationEngineTrait, snark::default_ck_hint}, }; @@ -855,6 +855,7 @@ mod tests { use core::{fmt::Write, marker::PhantomData}; use expect_test::{expect, Expect}; use ff::PrimeField; + use halo2curves::bn256::Bn256; type EE = provider::ipa_pc::EvaluationEngine; type S = spartan::snark::RelaxedR1CSSNARK; @@ -908,7 +909,7 @@ mod tests { } } - fn test_pp_digest_with(circuit1: &T1, circuit2: &T2, expected: &Expect) + fn test_pp_digest_with(circuit1: &T1, circuit2: &T2, expected: &Expect) where E1: Engine::Scalar>, E2: Engine::Scalar>, @@ -916,13 +917,12 @@ mod tests { E2::GE: DlogGroup, T1: StepCircuit, T2: StepCircuit, - // required to use the IPA in the initialization of the commitment key hints below - >::CommitmentKey: CommitmentKeyExtTrait, - >::CommitmentKey: CommitmentKeyExtTrait, + EE1: EvaluationEngineTrait, + EE2: EvaluationEngineTrait, { // this tests public parameters with a size specifically intended for a spark-compressed SNARK - let ck_hint1 = &*SPrime::>::ck_floor(); - let ck_hint2 = &*SPrime::>::ck_floor(); + let ck_hint1 = &*SPrime::::ck_floor(); + let ck_hint2 = &*SPrime::::ck_floor(); let pp = PublicParams::::setup(circuit1, circuit2, ck_hint1, ck_hint2); let digest_str = pp @@ -934,6 +934,7 @@ mod tests { let _ = write!(output, "{b:02x}"); output }); + expected.assert_eq(&digest_str); } @@ -943,13 +944,13 @@ mod tests { let trivial_circuit2 = TrivialCircuit::<::Scalar>::default(); let cubic_circuit1 = CubicCircuit::<::Scalar>::default(); - test_pp_digest_with::( + test_pp_digest_with::, EE<_>>( &trivial_circuit1, &trivial_circuit2, &expect!["9bc7ad2ab3f2a12455fdd21527598e365a14619c7f1e09f5cc3c78caa2fdd602"], ); - test_pp_digest_with::( + test_pp_digest_with::, EE<_>>( &cubic_circuit1, &trivial_circuit2, &expect!["8dea023ed642fd2d1a7bedb536cd96d22c0d25ea40961a4fe4a865169bf6ee01"], @@ -959,28 +960,40 @@ mod tests { let trivial_circuit2_grumpkin = TrivialCircuit::<::Scalar>::default(); let cubic_circuit1_grumpkin = CubicCircuit::<::Scalar>::default(); - test_pp_digest_with::( + test_pp_digest_with::, EE<_>>( &trivial_circuit1_grumpkin, &trivial_circuit2_grumpkin, &expect!["89e746ed5055445a4aceb2b6fb0413fe0bf4d2efec387dee85613922a972a701"], ); - test_pp_digest_with::( + test_pp_digest_with::, EE<_>>( &cubic_circuit1_grumpkin, &trivial_circuit2_grumpkin, &expect!["941f55146ac21a3b4ff9863546bea95df48cb0069d2fa9e8249f8d0a00560401"], ); + #[cfg(not(feature = "asm"))] + test_pp_digest_with::, EE<_>>( + &trivial_circuit1_grumpkin, + &trivial_circuit2_grumpkin, + &expect!["745e97ae39634db956cbcff2a2a34cb13fb85a109bd1f4c5770645de89373a01"], + ); + #[cfg(not(feature = "asm"))] + test_pp_digest_with::, EE<_>>( + &cubic_circuit1_grumpkin, + &trivial_circuit2_grumpkin, + &expect!["5ac34cf83ae67e4db63f2d03d52caa6f2ce8c51f7d9de4b9bfd26de2943b9d02"], + ); let trivial_circuit1_secp = TrivialCircuit::<::Scalar>::default(); let trivial_circuit2_secp = TrivialCircuit::<::Scalar>::default(); let cubic_circuit1_secp = CubicCircuit::<::Scalar>::default(); - test_pp_digest_with::( + test_pp_digest_with::, EE<_>>( &trivial_circuit1_secp, &trivial_circuit2_secp, &expect!["c70782c49d3de831b3822081655cf61c7d53533f0effcd5c4166cd4fbe651e00"], ); - test_pp_digest_with::( + test_pp_digest_with::, EE<_>>( &cubic_circuit1_secp, &trivial_circuit2_secp, &expect!["148c5994c443174b67699cb6169aa4489babebb360ae5145bb4b09d77a3a9a01"], @@ -1217,6 +1230,12 @@ mod tests { provider::hyperkzg::EvaluationEngine<_>, EE<_>, >(); + test_ivc_nontrivial_with_compression_with::< + Bn256EngineZM, + GrumpkinEngine, + ZMPCS, + EE<_>, + >(); } fn test_ivc_nontrivial_with_spark_compression_with() @@ -1314,6 +1333,12 @@ mod tests { test_ivc_nontrivial_with_spark_compression_with::, EE<_>>(); test_ivc_nontrivial_with_spark_compression_with::, EE<_>>( ); + test_ivc_nontrivial_with_spark_compression_with::< + Bn256EngineZM, + GrumpkinEngine, + ZMPCS, + EE<_>, + >(); } fn test_ivc_nondet_with_compression_with() @@ -1453,6 +1478,8 @@ mod tests { test_ivc_nondet_with_compression_with::, EE<_>>(); test_ivc_nondet_with_compression_with::, EE<_>>(); test_ivc_nondet_with_compression_with::, EE<_>>(); + test_ivc_nondet_with_compression_with::, EE<_>>( + ); } fn test_ivc_base_with() diff --git a/src/provider/ipa_pc.rs b/src/provider/ipa_pc.rs index 874d54975..3ac01b6d5 100644 --- a/src/provider/ipa_pc.rs +++ b/src/provider/ipa_pc.rs @@ -1,6 +1,6 @@ //! This module implements `EvaluationEngine` using an IPA-based polynomial commitment scheme use crate::{ - errors::NovaError, + errors::{NovaError, PCSError}, provider::{pedersen::CommitmentKeyExtTrait, traits::DlogGroup}, spartan::polys::eq::EqPolynomial, traits::{ @@ -403,7 +403,7 @@ where if P_hat == CE::::commit(&ck_hat.combine(&ck_c), &[self.a_hat, self.a_hat * b_hat]) { Ok(()) } else { - Err(NovaError::InvalidIPA) + Err(NovaError::PCSError(PCSError::InvalidIPA)) } } } diff --git a/src/provider/kzg_commitment.rs b/src/provider/kzg_commitment.rs new file mode 100644 index 000000000..37381e62b --- /dev/null +++ b/src/provider/kzg_commitment.rs @@ -0,0 +1,78 @@ +//! Commitment engine for KZG commitments +//! + +use std::marker::PhantomData; + +use ff::PrimeFieldBits; +use group::{prime::PrimeCurveAffine, Curve}; +use halo2curves::pairing::Engine; +use rand::rngs::StdRng; +use rand_core::SeedableRng; +use serde::{Deserialize, Serialize}; + +use crate::traits::{ + commitment::{CommitmentEngineTrait, Len}, + Engine as NovaEngine, Group, +}; + +use crate::provider::{ + non_hiding_kzg::{UVKZGCommitment, UVUniversalKZGParam}, + pedersen::Commitment, + traits::DlogGroup, +}; + +/// Provides a commitment engine +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct KZGCommitmentEngine { + _p: PhantomData, +} + +impl> CommitmentEngineTrait + for KZGCommitmentEngine +where + E::G1: DlogGroup, + E::G1Affine: Serialize + for<'de> Deserialize<'de>, + E::G2Affine: Serialize + for<'de> Deserialize<'de>, + E::Fr: PrimeFieldBits, // TODO due to use of gen_srs_for_testing, make optional +{ + type CommitmentKey = UVUniversalKZGParam; + type Commitment = Commitment; + + fn setup(label: &'static [u8], n: usize) -> Self::CommitmentKey { + // TODO: this is just for testing, replace by grabbing from a real setup for production + let mut bytes = [0u8; 32]; + let len = label.len().min(32); + bytes[..len].copy_from_slice(&label[..len]); + let rng = &mut StdRng::from_seed(bytes); + UVUniversalKZGParam::gen_srs_for_testing(rng, n.next_power_of_two()) + } + + fn commit(ck: &Self::CommitmentKey, v: &[::Scalar]) -> Self::Commitment { + assert!(ck.length() >= v.len()); + Commitment { + comm: E::G1::vartime_multiscalar_mul(v, &ck.powers_of_g[..v.len()]), + } + } +} + +impl> From> + for UVKZGCommitment +where + E::G1: Group, +{ + fn from(c: Commitment) -> Self { + UVKZGCommitment(c.comm.to_affine()) + } +} + +impl> From> + for Commitment +where + E::G1: Group, +{ + fn from(c: UVKZGCommitment) -> Self { + Commitment { + comm: c.0.to_curve(), + } + } +} diff --git a/src/provider/mod.rs b/src/provider/mod.rs index e0d6ce246..71d96b9d7 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -3,6 +3,7 @@ // public modules to be used as an evaluation engine with Spartan pub mod hyperkzg; pub mod ipa_pc; +pub mod non_hiding_zeromorph; // crate-public modules, made crate-public mostly for tests pub(crate) mod bn256_grumpkin; @@ -11,6 +12,10 @@ pub(crate) mod pedersen; pub(crate) mod poseidon; pub(crate) mod secp_secq; pub(crate) mod traits; +// a non-hiding variant of {kzg, zeromorph} +pub(crate) mod kzg_commitment; +pub(crate) mod non_hiding_kzg; +mod util; // crate-private modules mod keccak; @@ -25,8 +30,11 @@ use crate::{ }, traits::Engine, }; +use halo2curves::bn256::Bn256; use pasta_curves::{pallas, vesta}; +use self::kzg_commitment::KZGCommitmentEngine; + /// An implementation of the Nova `Engine` trait with BN254 curve and Pedersen commitment scheme #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Bn256Engine; @@ -55,6 +63,20 @@ impl Engine for GrumpkinEngine { type CE = PedersenCommitmentEngine; } +/// An implementation of the Nova `Engine` trait with BN254 curve and Zeromorph commitment scheme +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Bn256EngineZM; + +impl Engine for Bn256EngineZM { + type Base = bn256::Base; + type Scalar = bn256::Scalar; + type GE = bn256::Point; + type RO = PoseidonRO; + type ROCircuit = PoseidonROCircuit; + type TE = Keccak256Transcript; + type CE = KZGCommitmentEngine; +} + /// An implementation of the Nova `Engine` trait with Secp256k1 curve and Pedersen commitment scheme #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Secp256k1Engine; diff --git a/src/provider/non_hiding_kzg.rs b/src/provider/non_hiding_kzg.rs new file mode 100644 index 000000000..a2aaf12b7 --- /dev/null +++ b/src/provider/non_hiding_kzg.rs @@ -0,0 +1,347 @@ +//! Non-hiding variant of KZG10 scheme for univariate polynomials. +use ff::{Field, PrimeField, PrimeFieldBits}; +use group::{prime::PrimeCurveAffine, Curve, Group as _}; +use halo2curves::pairing::{Engine, MillerLoopResult, MultiMillerLoop}; +use rand_core::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Borrow, marker::PhantomData, ops::Mul}; + +use crate::{ + errors::{NovaError, PCSError}, + provider::traits::DlogGroup, + provider::util::fb_msm, + traits::{commitment::Len, Group, TranscriptReprTrait}, +}; + +/// `UniversalParams` are the universal parameters for the KZG10 scheme. +#[derive(Debug, Clone, Eq, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize, E::G2Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" +))] +pub struct UVUniversalKZGParam { + /// Group elements of the form `{ β^i G }`, where `i` ranges from 0 to + /// `degree`. + pub powers_of_g: Vec, + /// Group elements of the form `{ β^i H }`, where `i` ranges from 0 to + /// `degree`. + pub powers_of_h: Vec, +} + +impl PartialEq for UVUniversalKZGParam { + fn eq(&self, other: &UVUniversalKZGParam) -> bool { + self.powers_of_g == other.powers_of_g && self.powers_of_h == other.powers_of_h + } +} + +// for the purpose of the Len trait, we count commitment bases, i.e. G1 elements +impl Len for UVUniversalKZGParam { + fn length(&self) -> usize { + self.powers_of_g.len() + } +} + +/// `UnivariateProverKey` is used to generate a proof +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>" +))] +pub struct UVKZGProverKey { + /// generators + pub powers_of_g: Vec, +} + +/// `UVKZGVerifierKey` is used to check evaluation proofs for a given +/// commitment. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize, E::G2Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" +))] +pub struct UVKZGVerifierKey { + /// The generator of G1. + pub g: E::G1Affine, + /// The generator of G2. + pub h: E::G2Affine, + /// β times the above generator of G2. + pub beta_h: E::G2Affine, +} + +impl UVUniversalKZGParam { + /// Returns the maximum supported degree + pub fn max_degree(&self) -> usize { + self.powers_of_g.len() + } + + /// Returns the prover parameters + /// + /// # Panics + /// if `supported_size` is greater than `self.max_degree()` + pub fn extract_prover_key(&self, supported_size: usize) -> UVKZGProverKey { + let powers_of_g = self.powers_of_g[..=supported_size].to_vec(); + UVKZGProverKey { powers_of_g } + } + + /// Returns the verifier parameters + /// + /// # Panics + /// If self.prover_params is empty. + pub fn extract_verifier_key(&self, supported_size: usize) -> UVKZGVerifierKey { + assert!( + self.powers_of_g.len() >= supported_size, + "supported_size is greater than self.max_degree()" + ); + UVKZGVerifierKey { + g: self.powers_of_g[0], + h: self.powers_of_h[0], + beta_h: self.powers_of_h[1], + } + } + + /// Trim the universal parameters to specialize the public parameters + /// for univariate polynomials to the given `supported_size`, and + /// returns prover key and verifier key. `supported_size` should + /// be in range `1..params.len()` + /// + /// # Panics + /// If `supported_size` is greater than `self.max_degree()`, or `self.max_degree()` is zero. + pub fn trim(&self, supported_size: usize) -> (UVKZGProverKey, UVKZGVerifierKey) { + let powers_of_g = self.powers_of_g[..=supported_size].to_vec(); + + let pk = UVKZGProverKey { powers_of_g }; + let vk = UVKZGVerifierKey { + g: self.powers_of_g[0], + h: self.powers_of_h[0], + beta_h: self.powers_of_h[1], + }; + (pk, vk) + } +} + +impl UVUniversalKZGParam +where + E::Fr: PrimeFieldBits, +{ + /// Build SRS for testing. + /// WARNING: THIS FUNCTION IS FOR TESTING PURPOSE ONLY. + /// THE OUTPUT SRS SHOULD NOT BE USED IN PRODUCTION. + pub fn gen_srs_for_testing(mut rng: &mut R, max_degree: usize) -> Self { + let beta = E::Fr::random(&mut rng); + let g = E::G1::random(&mut rng); + let h = E::G2::random(rng); + + let nz_powers_of_beta = (0..=max_degree) + .scan(beta, |acc, _| { + let val = *acc; + *acc *= beta; + Some(val) + }) + .collect::>(); + + let window_size = fb_msm::get_mul_window_size(max_degree); + let scalar_bits = E::Fr::NUM_BITS as usize; + + let (powers_of_g_projective, powers_of_h_projective) = rayon::join( + || { + let g_table = fb_msm::get_window_table(scalar_bits, window_size, g); + fb_msm::multi_scalar_mul::(scalar_bits, window_size, &g_table, &nz_powers_of_beta) + }, + || { + let h_table = fb_msm::get_window_table(scalar_bits, window_size, h); + fb_msm::multi_scalar_mul::(scalar_bits, window_size, &h_table, &nz_powers_of_beta) + }, + ); + + let mut powers_of_g = vec![E::G1Affine::identity(); powers_of_g_projective.len()]; + let mut powers_of_h = vec![E::G2Affine::identity(); powers_of_h_projective.len()]; + + rayon::join( + || E::G1::batch_normalize(&powers_of_g_projective, &mut powers_of_g), + || E::G2::batch_normalize(&powers_of_h_projective, &mut powers_of_h), + ); + + Self { + powers_of_g, + powers_of_h, + } + } +} +/// Commitments +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>" +))] +pub struct UVKZGCommitment( + /// the actual commitment is an affine point. + pub E::G1Affine, +); + +impl TranscriptReprTrait for UVKZGCommitment +where + E::G1: DlogGroup, + // Note: due to the move of the bound TranscriptReprTrait on G::Base from Group to Engine + ::Base: TranscriptReprTrait, +{ + fn to_transcript_bytes(&self) -> Vec { + // TODO: avoid the round-trip through the group (to_curve .. to_coordinates) + let (x, y, is_infinity) = self.0.to_curve().to_coordinates(); + let is_infinity_byte = (!is_infinity).into(); + [ + x.to_transcript_bytes(), + y.to_transcript_bytes(), + [is_infinity_byte].to_vec(), + ] + .concat() + } +} + +/// Polynomial Evaluation +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct UVKZGEvaluation(pub E::Fr); + +#[derive(Debug, Clone, Eq, PartialEq, Default)] + +/// Proofs +pub struct UVKZGProof { + /// proof + pub proof: E::G1Affine, +} + +/// Polynomial and its associated types +pub type UVKZGPoly = crate::spartan::polys::univariate::UniPoly; + +#[derive(Debug, Clone, Eq, PartialEq, Default)] +/// KZG Polynomial Commitment Scheme on univariate polynomial. +/// Note: this is non-hiding, which is why we will implement traits on this token struct, +/// as we expect to have several impls for the trait pegged on the same instance of a pairing::Engine. +#[allow(clippy::upper_case_acronyms)] +pub struct UVKZGPCS { + #[doc(hidden)] + phantom: PhantomData, +} + +impl UVKZGPCS +where + E::G1: DlogGroup, +{ + /// Generate a commitment for a polynomial + /// Note that the scheme is not hidding + pub fn commit( + prover_param: impl Borrow>, + poly: &UVKZGPoly, + ) -> Result, NovaError> { + let prover_param = prover_param.borrow(); + + if poly.degree() > prover_param.powers_of_g.len() { + return Err(NovaError::PCSError(PCSError::LengthError)); + } + let C = ::vartime_multiscalar_mul( + poly.coeffs.as_slice(), + &prover_param.powers_of_g.as_slice()[..poly.coeffs.len()], + ); + Ok(UVKZGCommitment(C.to_affine())) + } + + /// On input a polynomial `p` and a point `point`, outputs a proof for the + /// same. + pub fn open( + prover_param: impl Borrow>, + polynomial: &UVKZGPoly, + point: &E::Fr, + ) -> Result<(UVKZGProof, UVKZGEvaluation), NovaError> { + let prover_param = prover_param.borrow(); + let divisor = UVKZGPoly { + coeffs: vec![-*point, E::Fr::ONE], + }; + let witness_polynomial = polynomial + .divide_with_q_and_r(&divisor) + .map(|(q, _r)| q) + .ok_or(NovaError::PCSError(PCSError::ZMError))?; + let proof = ::vartime_multiscalar_mul( + witness_polynomial.coeffs.as_slice(), + &prover_param.powers_of_g.as_slice()[..witness_polynomial.coeffs.len()], + ); + let evaluation = UVKZGEvaluation(polynomial.evaluate(point)); + + Ok(( + UVKZGProof { + proof: proof.to_affine(), + }, + evaluation, + )) + } + + /// Verifies that `value` is the evaluation at `x` of the polynomial + /// committed inside `comm`. + #[allow(dead_code)] + pub fn verify( + verifier_param: impl Borrow>, + commitment: &UVKZGCommitment, + point: &E::Fr, + proof: &UVKZGProof, + evaluation: &UVKZGEvaluation, + ) -> Result { + let verifier_param = verifier_param.borrow(); + + let pairing_inputs: Vec<(E::G1Affine, E::G2Prepared)> = vec![ + ( + (verifier_param.g.mul(evaluation.0) - proof.proof.mul(point) - commitment.0.to_curve()) + .to_affine(), + verifier_param.h.into(), + ), + (proof.proof, verifier_param.beta_h.into()), + ]; + let pairing_input_refs = pairing_inputs + .iter() + .map(|(a, b)| (a, b)) + .collect::>(); + let pairing_result = E::multi_miller_loop(pairing_input_refs.as_slice()).final_exponentiation(); + Ok(pairing_result.is_identity().into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::spartan::polys::univariate::UniPoly; + use rand::{thread_rng, Rng}; + use rand_core::{CryptoRng, RngCore}; + + fn random(degree: usize, mut rng: &mut R) -> UVKZGPoly { + let coeffs = (0..=degree).map(|_| F::random(&mut rng)).collect(); + UniPoly::new(coeffs) + } + + fn end_to_end_test_template() -> Result<(), NovaError> + where + E: MultiMillerLoop, + E::G1: DlogGroup, + E::Fr: PrimeFieldBits, + { + for _ in 0..100 { + let mut rng = &mut thread_rng(); + let degree = rng.gen_range(2..20); + + let pp = UVUniversalKZGParam::::gen_srs_for_testing(&mut rng, degree); + let (ck, vk) = pp.trim(degree); + let p = random(degree, rng); + let comm = UVKZGPCS::::commit(&ck, &p)?; + let point = E::Fr::random(rng); + let (proof, value) = UVKZGPCS::::open(&ck, &p, &point)?; + assert!( + UVKZGPCS::::verify(&vk, &comm, &point, &proof, &value)?, + "proof was incorrect for max_degree = {}, polynomial_degree = {}", + degree, + p.degree(), + ); + } + Ok(()) + } + + #[test] + fn end_to_end_test() { + end_to_end_test_template::().expect("test failed for Bn256"); + } +} diff --git a/src/provider/non_hiding_zeromorph.rs b/src/provider/non_hiding_zeromorph.rs new file mode 100644 index 000000000..b6c10d9a4 --- /dev/null +++ b/src/provider/non_hiding_zeromorph.rs @@ -0,0 +1,800 @@ +//! Non-hiding Zeromorph scheme for Multilinear Polynomials. +//! +//! + +use crate::{ + errors::{NovaError, PCSError}, + provider::{ + non_hiding_kzg::{ + UVKZGCommitment, UVKZGEvaluation, UVKZGPoly, UVKZGProof, UVKZGProverKey, UVKZGVerifierKey, + UVUniversalKZGParam, UVKZGPCS, + }, + traits::DlogGroup, + }, + spartan::polys::multilinear::MultilinearPolynomial, + traits::{ + commitment::Len, evaluation::EvaluationEngineTrait, Engine as NovaEngine, Group, + TranscriptEngineTrait, TranscriptReprTrait, + }, + Commitment, +}; +use ff::{BatchInvert, Field, PrimeField, PrimeFieldBits}; +use group::{Curve, Group as _}; +use itertools::Itertools as _; +use halo2curves::pairing::{Engine, MillerLoopResult, MultiMillerLoop}; +use rayon::{ + iter::IntoParallelRefIterator, + prelude::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}, +}; +use ref_cast::RefCast; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{borrow::Borrow, iter, marker::PhantomData}; + +use crate::provider::kzg_commitment::KZGCommitmentEngine; + +/// `ZMProverKey` is used to generate a proof +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>" +))] +pub struct ZMProverKey { + commit_pp: UVKZGProverKey, + open_pp: UVKZGProverKey, +} + +/// `ZMVerifierKey` is used to check evaluation proofs for a given +/// commitment. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize, E::G2Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" +))] +pub struct ZMVerifierKey { + vp: UVKZGVerifierKey, + s_offset_h: E::G2Affine, +} + +/// Trim the universal parameters to specialize the public parameters +/// for multilinear polynomials to the given `max_degree`, and +/// returns prover key and verifier key. `supported_size` should +/// be in range `1..params.len()` +/// +/// # Panics +/// If `supported_size` is greater than `self.max_degree()`, or `self.max_degree()` is zero. +// +// TODO: important, we need a better way to handle that the commitment key should be 2^max_degree sized, +// see the runtime error in commit() below +pub fn trim( + params: &UVUniversalKZGParam, + max_degree: usize, +) -> (ZMProverKey, ZMVerifierKey) { + let (commit_pp, vp) = params.trim(max_degree); + let offset = params.powers_of_g.len() - max_degree; + let open_pp = { + let offset_powers_of_g1 = params.powers_of_g[offset..].to_vec(); + UVKZGProverKey { + powers_of_g: offset_powers_of_g1, + } + }; + let s_offset_h = params.powers_of_h[offset]; + + ( + ZMProverKey { commit_pp, open_pp }, + ZMVerifierKey { vp, s_offset_h }, + ) +} + +/// Commitments +#[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] +pub struct ZMCommitment( + /// the actual commitment is an affine point. + pub E::G1Affine, +); + +impl From> for ZMCommitment { + fn from(value: UVKZGCommitment) -> Self { + ZMCommitment(value.0) + } +} + +impl From> for UVKZGCommitment { + fn from(value: ZMCommitment) -> Self { + UVKZGCommitment(value.0) + } +} + +/// Polynomial Evaluation +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct ZMEvaluation(E::Fr); + +impl From> for ZMEvaluation { + fn from(value: UVKZGEvaluation) -> Self { + ZMEvaluation(value.0) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>" +))] +/// Proofs +pub struct ZMProof { + /// proof + pub pi: E::G1Affine, + /// Polynomial commitment to qhat + pub cqhat: UVKZGCommitment, + /// Polynomial commitment to qk + pub ck: Vec>, +} + +#[derive(Debug, Clone, Eq, PartialEq, Default)] +/// Zeromorph Polynomial Commitment Scheme on multilinear polynomials. +/// Note: this is non-hiding, which is why we will implement the EvaluationEngineTrait on this token struct, +/// as we will have several impls for the trait pegged on the same instance of a pairing::Engine. +#[allow(clippy::upper_case_acronyms)] +pub struct ZMPCS { + #[doc(hidden)] + phantom: PhantomData<(E, NE)>, +} + +impl> ZMPCS +where + E::G1: DlogGroup, + // Note: due to the move of the bound TranscriptReprTrait on G::Base from Group to Engine + ::Base: TranscriptReprTrait, +{ + const fn protocol_name() -> &'static [u8] { + b"Zeromorph" + } + + /// Generate a commitment for a polynomial + /// Note that the scheme is not hidding + pub fn commit( + pp: impl Borrow>, + poly: &MultilinearPolynomial, + ) -> Result, NovaError> { + let pp = pp.borrow(); + if pp.commit_pp.powers_of_g.len() < poly.Z.len() { + return Err(PCSError::LengthError.into()); + } + UVKZGPCS::commit(&pp.commit_pp, UVKZGPoly::ref_cast(&poly.Z)).map(|c| c.into()) + } + + /// On input a polynomial `poly` and a point `point`, outputs a proof for the + /// same. + pub fn open( + pp: &impl Borrow>, + comm: &ZMCommitment, + poly: &MultilinearPolynomial, + point: &[E::Fr], + eval: &ZMEvaluation, + transcript: &mut impl TranscriptEngineTrait, + ) -> Result, NovaError> { + transcript.dom_sep(Self::protocol_name()); + + let pp = pp.borrow(); + if pp.commit_pp.powers_of_g.len() < poly.Z.len() { + return Err(NovaError::PCSError(PCSError::LengthError)); + } + + debug_assert_eq!(Self::commit(pp, poly).unwrap().0, comm.0); + debug_assert_eq!(poly.evaluate(point), eval.0); + + let (quotients, remainder) = quotients(poly, point); + debug_assert_eq!(quotients.len(), poly.get_num_vars()); + debug_assert_eq!(remainder, eval.0); + + // Compute the multilinear quotients q_k = q_k(X_0, ..., X_{k-1}) + let quotients_polys = quotients + .into_iter() + .map(UVKZGPoly::new) + .collect::>(); + + // Compute and absorb commitments C_{q_k} = [q_k], k = 0,...,d-1 + let q_comms = quotients_polys + .par_iter() + .map(|q| UVKZGPCS::commit(&pp.commit_pp, q)) + .collect::, _>>()?; + q_comms.iter().for_each(|c| transcript.absorb(b"quo", c)); + + // Get challenge y + let y = transcript.squeeze(b"y")?; + + // Compute the batched, lifted-degree quotient `\hat{q}` + // qq_hat = ∑_{i=0}^{num_vars-1} y^i * X^(2^num_vars - d_k - 1) * q_i(x) + let q_hat = batched_lifted_degree_quotient(y, "ients_polys); + // Compute and absorb the commitment C_q = [\hat{q}] + let q_hat_comm = UVKZGPCS::commit(&pp.commit_pp, &q_hat)?; + transcript.absorb(b"q_hat", &q_hat_comm); + + // Get challenges x and z + let x = transcript.squeeze(b"x")?; + let z = transcript.squeeze(b"z")?; + + // Compute batched degree and ZM-identity quotient polynomial pi + let (eval_scalar, (degree_check_q_scalars, zmpoly_q_scalars)) = + eval_and_quotient_scalars(y, x, z, point); + // f = z * poly.Z + q_hat + (-z * Φ_n(x) * e) + ∑_k (q_scalars_k * q_k) + let mut f = UVKZGPoly::new(poly.Z.clone()); + f *= &z; + f += &q_hat; + f[0] += eval_scalar * eval.0; + quotients_polys + .into_iter() + .zip_eq(degree_check_q_scalars) + .zip_eq(zmpoly_q_scalars) + .for_each(|((mut q, degree_check_scalar), zm_poly_scalar)| { + q *= &(degree_check_scalar + zm_poly_scalar); + f += &q; + }); + debug_assert_eq!(f.evaluate(&x), E::Fr::ZERO); + // hence uveval == Fr::ZERO + + // Compute and send proof commitment pi + let (uvproof, _uveval): (UVKZGProof<_>, UVKZGEvaluation<_>) = + UVKZGPCS::::open(&pp.open_pp, &f, &x).map(|(proof, eval)| (proof, eval))?; + + let proof = ZMProof { + pi: uvproof.proof, + cqhat: q_hat_comm, + ck: q_comms, + }; + + Ok(proof) + } + + /// Verifies that `value` is the evaluation at `x` of the polynomial + /// committed inside `comm`. + pub fn verify( + vk: &impl Borrow>, + transcript: &mut impl TranscriptEngineTrait, + comm: &ZMCommitment, + point: &[E::Fr], + evaluation: &ZMEvaluation, + proof: &ZMProof, + ) -> Result { + transcript.dom_sep(Self::protocol_name()); + + let vk = vk.borrow(); + + // Receive commitments [q_k] + proof.ck.iter().for_each(|c| transcript.absorb(b"quo", c)); + + // Challenge y + let y = transcript.squeeze(b"y")?; + + // Receive commitment C_{q} + transcript.absorb(b"q_hat", &proof.cqhat); + + // Challenges x, z + let x = transcript.squeeze(b"x")?; + let z = transcript.squeeze(b"z")?; + + let (eval_scalar, (mut q_scalars, zmpoly_q_scalars)) = + eval_and_quotient_scalars(y, x, z, point); + q_scalars + .iter_mut() + .zip_eq(zmpoly_q_scalars) + .for_each(|(scalar, zm_poly_scalar)| { + *scalar += zm_poly_scalar; + }); + let scalars = [vec![E::Fr::ONE, z, eval_scalar * evaluation.0], q_scalars].concat(); + let bases = [ + vec![proof.cqhat.0, comm.0, vk.vp.g], + proof.ck.iter().map(|c| c.0).collect(), + ] + .concat(); + let c = ::vartime_multiscalar_mul(&scalars, &bases).to_affine(); + + let pi = proof.pi; + + let pairing_inputs = [ + (&c, &(-vk.s_offset_h).into()), + ( + &pi, + &(E::G2::from(vk.vp.beta_h) - (vk.vp.h * x)) + .to_affine() + .into(), + ), + ]; + + let pairing_result = E::multi_miller_loop(&pairing_inputs).final_exponentiation(); + Ok(pairing_result.is_identity().into()) + } +} + +/// Computes the quotient polynomials of a given multilinear polynomial with respect to a specific input point. +/// +/// Given a multilinear polynomial `poly` and a point `point`, this function calculates the quotient polynomials `q_k` +/// and the evaluation at `point`, such that: +/// +/// ```text +/// poly - poly(point) = Σ (X_k - point_k) * q_k(X_0, ..., X_{k-1}) +/// ``` +/// +/// where `poly(point)` is the evaluation of `poly` at `point`, and each `q_k` is a polynomial in `k` variables. +/// +/// Since our evaluations are presented in order reverse from the coefficients, if we want to interpret index q_k +/// to be the k-th coefficient in the polynomials returned here, the equality that holds is: +/// +/// ```text +/// poly - poly(point) = Σ (X_{n-1-k} - point_{n-1-k}) * q_k(X_0, ..., X_{k-1}) +/// ``` +/// +fn quotients(poly: &MultilinearPolynomial, point: &[F]) -> (Vec>, F) { + let num_var = poly.get_num_vars(); + assert_eq!(num_var, point.len()); + + let mut remainder = poly.Z.to_vec(); + let mut quotients = point + .iter() + .enumerate() + .map(|(idx, x_i)| { + let (remainder_lo, remainder_hi) = remainder.split_at_mut(1 << (num_var - 1 - idx)); + let mut quotient = vec![F::ZERO; remainder_lo.len()]; + + quotient + .par_iter_mut() + .zip_eq(&*remainder_lo) + .zip_eq(&*remainder_hi) + .for_each(|((q, r_lo), r_hi)| { + *q = *r_hi - *r_lo; + }); + remainder_lo + .par_iter_mut() + .zip_eq(remainder_hi) + .for_each(|(r_lo, r_hi)| { + *r_lo += (*r_hi - r_lo as &_) * x_i; + }); + + remainder.truncate(1 << (num_var - 1 - idx)); + + quotient + }) + .collect::>>(); + quotients.reverse(); + + (quotients, remainder[0]) +} + +// Compute the batched, lifted-degree quotient `\hat{q}` +fn batched_lifted_degree_quotient( + y: F, + quotients_polys: &[UVKZGPoly], +) -> UVKZGPoly { + let num_vars = quotients_polys.len(); + + let powers_of_y = (0..num_vars) + .scan(F::ONE, |acc, _| { + let val = *acc; + *acc *= y; + Some(val) + }) + .collect::>(); + + #[allow(clippy::disallowed_methods)] + let q_hat = powers_of_y + .iter() + .zip_eq(quotients_polys.iter().map(|qp| qp.as_ref())) + .enumerate() + .fold( + vec![F::ZERO; 1 << num_vars], + |mut q_hat, (idx, (power_of_y, q))| { + let offset = q_hat.len() - (1 << idx); + q_hat[offset..] + .par_iter_mut() + .zip(q) + .for_each(|(q_hat, q)| { + *q_hat += *power_of_y * *q; + }); + q_hat + }, + ); + UVKZGPoly::new(q_hat) +} + +/// Computes some key terms necessary for computing the partially evaluated univariate ZM polynomial +fn eval_and_quotient_scalars(y: F, x: F, z: F, point: &[F]) -> (F, (Vec, Vec)) { + let num_vars = point.len(); + + // squares_of_x = [x, x^2, .. x^{2^k}, .. x^{2^num_vars}] + let squares_of_x = iter::successors(Some(x), |&x| Some(x.square())) + .take(num_vars + 1) + .collect::>(); + // offsets_of_x = [Π_{j=i}^{num_vars-1} x^(2^j), i ∈ [0, num_vars-1]] = [x^(2^num_vars - d_i - 1), i ∈ [0, num_vars-1]] + let offsets_of_x = { + let mut offsets_of_x = squares_of_x + .iter() + .rev() + .skip(1) + .scan(F::ONE, |state, power_of_x| { + *state *= power_of_x; + Some(*state) + }) + .collect::>(); + offsets_of_x.reverse(); + offsets_of_x + }; + + // vs = [ (x^(2^num_vars) - 1) / (x^(2^i) - 1), i ∈ [0, num_vars-1]] + // Note Φ_(n-i)(x^(2^i)) = (x^(2^i))^(2^(n-i) - 1) / (x^(2^i) - 1) = (x^(2^num_vars) - 1) / (x^(2^i) - 1) = vs[i] + // Φ_(n-i-1)(x^(2^(i+1))) = (x^(2^(i+1)))^(2^(n-i-1)) - 1 / (x^(2^(i+1)) - 1) = (x^(2^num_vars) - 1) / (x^(2^(i+1)) - 1) = vs[i+1] + let vs = { + let v_numer = squares_of_x[num_vars] - F::ONE; + let mut v_denoms = squares_of_x + .iter() + .map(|square_of_x| *square_of_x - F::ONE) + .collect::>(); + v_denoms.iter_mut().batch_invert(); + v_denoms + .iter() + .map(|v_denom| v_numer * v_denom) + .collect::>() + }; + + // q_scalars = [- (y^i * x^(2^num_vars - d_i - 1) + z * (x^(2^i) * vs[i+1] - u_i * vs[i])), i ∈ [0, num_vars-1]] + // = [- (y^i * x^(2^num_vars - d_i - 1) + z * (x^(2^i) * Φ_(n-i-1)(x^(2^(i+1))) - u_i * Φ_(n-i)(x^(2^i)))), i ∈ [0, num_vars-1]] + #[allow(clippy::disallowed_methods)] + let q_scalars = iter::successors(Some(F::ONE), |acc| Some(*acc * y)).take(num_vars) + .zip_eq(offsets_of_x) + // length: num_vars + 1 + .zip(squares_of_x) + // length: num_vars + 1 + .zip(&vs) + .zip_eq(&vs[1..]) + .zip_eq(point.iter().rev()) // assume variables come in BE form + .map( + |(((((power_of_y, offset_of_x), square_of_x), v_i), v_j), u_i)| { + (-(power_of_y * offset_of_x), -(z * (square_of_x * v_j - *u_i * v_i))) + }, + ) + .unzip(); + + // -vs[0] * z = -z * (x^(2^num_vars) - 1) / (x - 1) = -z Φ_n(x) + (-vs[0] * z, q_scalars) +} + +impl>> + EvaluationEngineTrait for ZMPCS +where + E::G1: DlogGroup, + E::G1Affine: Serialize + DeserializeOwned, + E::G2Affine: Serialize + DeserializeOwned, + ::Base: TranscriptReprTrait, // Note: due to the move of the bound TranscriptReprTrait on G::Base from Group to Engine + E::Fr: PrimeFieldBits, // TODO due to use of gen_srs_for_testing, make optional +{ + type ProverKey = ZMProverKey; + type VerifierKey = ZMVerifierKey; + + type EvaluationArgument = ZMProof; + + fn setup(ck: &UVUniversalKZGParam) -> (Self::ProverKey, Self::VerifierKey) { + trim(ck, ck.length() - 1) + } + + fn prove( + _ck: &UVUniversalKZGParam, + pk: &Self::ProverKey, + transcript: &mut NE::TE, + comm: &Commitment, + poly: &[NE::Scalar], + point: &[NE::Scalar], + eval: &NE::Scalar, + ) -> Result { + let commitment = ZMCommitment::from(UVKZGCommitment::from(*comm)); + let polynomial = MultilinearPolynomial::new(poly.to_vec()); + let evaluation = ZMEvaluation(*eval); + + ZMPCS::open(pk, &commitment, &polynomial, point, &evaluation, transcript) + } + + fn verify( + vk: &Self::VerifierKey, + transcript: &mut NE::TE, + comm: &Commitment, + point: &[NE::Scalar], + eval: &NE::Scalar, + arg: &Self::EvaluationArgument, + ) -> Result<(), NovaError> { + let commitment = ZMCommitment::from(UVKZGCommitment::from(*comm)); + let evaluation = ZMEvaluation(*eval); + + if !ZMPCS::verify(vk, transcript, &commitment, point, &evaluation, arg)? { + return Err(NovaError::UnSat); + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::iter; + + use ff::{Field, PrimeField, PrimeFieldBits}; + use halo2curves::bn256::Bn256; + use halo2curves::bn256::Fr as Scalar; + use itertools::Itertools as _; + use halo2curves::pairing::MultiMillerLoop; + use rand::thread_rng; + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + use super::quotients; + use crate::{ + provider::{ + keccak::Keccak256Transcript, + non_hiding_kzg::{UVKZGPoly, UVUniversalKZGParam}, + non_hiding_zeromorph::{ + batched_lifted_degree_quotient, eval_and_quotient_scalars, trim, ZMEvaluation, ZMPCS, + }, + traits::DlogGroup, + Bn256Engine, + }, + spartan::polys::multilinear::MultilinearPolynomial, + traits::{Engine as NovaEngine, Group, TranscriptEngineTrait, TranscriptReprTrait}, + }; + + fn commit_open_verify_with>() + where + E::G1: DlogGroup, + ::Base: TranscriptReprTrait, // Note: due to the move of the bound TranscriptReprTrait on G::Base from Group to Engine + E::Fr: PrimeFieldBits, + { + let max_vars = 16; + let mut rng = thread_rng(); + let max_poly_size = 1 << (max_vars + 1); + let universal_setup = UVUniversalKZGParam::::gen_srs_for_testing(&mut rng, max_poly_size); + + for num_vars in 3..max_vars { + // Setup + let (pp, vk) = { + let poly_size = 1 << (num_vars + 1); + + trim(&universal_setup, poly_size) + }; + + // Commit and open + let mut transcript = Keccak256Transcript::::new(b"test"); + let poly = MultilinearPolynomial::::random(num_vars, &mut thread_rng()); + let comm = ZMPCS::::commit(&pp, &poly).unwrap(); + let point = iter::from_fn(|| transcript.squeeze(b"pt").ok()) + .take(num_vars) + .collect::>(); + let eval = ZMEvaluation(poly.evaluate(&point)); + + let mut transcript_prover = Keccak256Transcript::::new(b"test"); + let proof = ZMPCS::open(&pp, &comm, &poly, &point, &eval, &mut transcript_prover).unwrap(); + + // Verify + let mut transcript_verifier = Keccak256Transcript::::new(b"test"); + let result = ZMPCS::verify( + &vk, + &mut transcript_verifier, + &comm, + point.as_slice(), + &eval, + &proof, + ); + + // check both random oracles are synced, as expected + assert_eq!( + transcript_prover.squeeze(b"test"), + transcript_verifier.squeeze(b"test") + ); + + result.unwrap(); + } + } + + #[test] + fn test_commit_open_verify() { + commit_open_verify_with::(); + } + + #[test] + fn test_quotients() { + // Define size parameters + let num_vars = 4; // Example number of variables for the multilinear polynomial + + // Construct a random multilinear polynomial f, and u such that f(u) = v. + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let poly = MultilinearPolynomial::random(num_vars, &mut rng); + let u_challenge: Vec<_> = (0..num_vars).map(|_| Scalar::random(&mut rng)).collect(); + let v_evaluation = poly.evaluate(&u_challenge); + + // Compute the multilinear quotients q_k = q_k(X_0, ..., X_{k-1}) + let (quotients, constant_term) = quotients(&poly, &u_challenge); + + // Assert that the constant term is equal to v_evaluation + assert_eq!(constant_term, v_evaluation, "The constant term should be equal to the evaluation of the polynomial at the challenge point."); + + // Check that the identity holds for a random evaluation point z + // poly - poly(z) = Σ (X_k - z_k) * q_k(X_0, ..., X_{k-1}) + // except for our inversion of coefficient order in polynomials and points (see below) + let z_challenge: Vec<_> = (0..num_vars).map(|_| Scalar::random(&mut rng)).collect(); + let mut result = poly.evaluate(&z_challenge); + result -= v_evaluation; + + for (k, q_k) in quotients.iter().enumerate() { + let q_k_poly = MultilinearPolynomial::new(q_k.clone()); + // the following looks weird because the quotient polynomials are coefficiented in reverse order from evaluation + // IOW in 'normal evaluation order' this should be let z_partial = &z_challenge[..k]; + let z_partial = &z_challenge[z_challenge.len() - k..]; + + let q_k_eval = q_k_poly.evaluate(z_partial); + // the following looks weird because the quotient polynomials are coefficiented in reverse order from evaluation + // IOW in 'normal evaluation order' this should be + // result -= (z_challenge[k] - u_challenge[k]) * q_k_eval; + result -= (z_challenge[z_challenge.len() - k - 1] - u_challenge[z_challenge.len() - k - 1]) + * q_k_eval; + } + + // Assert that the result is zero, which verifies the correctness of the quotients + assert!( + bool::from(result.is_zero()), + "The computed quotients should satisfy the polynomial identity." + ); + } + + #[test] + fn test_batched_lifted_degree_quotient() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + + let num_vars = 3; + let n = 1 << num_vars; // Assuming N = 2^num_vars + + // Define mock q_k with deg(q_k) = 2^k - 1 + let q_0 = UVKZGPoly::new(vec![Scalar::one()]); + let q_1 = UVKZGPoly::new(vec![Scalar::from(2), Scalar::from(3)]); + let q_2 = UVKZGPoly::new(vec![ + Scalar::from(4), + Scalar::from(5), + Scalar::from(6), + Scalar::from(7), + ]); + let quotients = vec![q_0, q_1, q_2]; + + // Generate a random y challenge + let y_challenge = Scalar::random(&mut rng); + + // Compute batched quotient \hat{q} using the function + let batched_quotient = batched_lifted_degree_quotient(y_challenge, "ients); + + // Now explicitly define q_k_lifted = X^{N-2^k} * q_k and compute the expected batched result + let q_0_lifted = [vec![Scalar::zero(); n - 1], vec![Scalar::one()]].concat(); + let q_1_lifted = [ + vec![Scalar::zero(); n - 2], + vec![Scalar::from(2), Scalar::from(3)], + ] + .concat(); + let q_2_lifted = [ + vec![Scalar::zero(); n - 4], + vec![ + Scalar::from(4), + Scalar::from(5), + Scalar::from(6), + Scalar::from(7), + ], + ] + .concat(); + + // Explicitly compute \hat{q} + let mut batched_quotient_expected = vec![Scalar::zero(); n]; + batched_quotient_expected + .iter_mut() + .zip_eq(q_0_lifted) + .zip_eq(q_1_lifted) + .zip_eq(q_2_lifted) + .for_each(|(((res, q_0), q_1), q_2)| { + *res += q_0 + y_challenge * q_1 + y_challenge * y_challenge * q_2; + }); + + // Compare the computed and expected batched quotients + assert_eq!(batched_quotient, UVKZGPoly::new(batched_quotient_expected)); + } + + #[test] + fn test_partially_evaluated_quotient_zeta() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + + let num_vars = 3; + + // Define some mock q_k with deg(q_k) = 2^k - 1 + let _q_0 = UVKZGPoly::new(vec![Scalar::one()]); + let _q_1 = UVKZGPoly::new(vec![Scalar::from(2), Scalar::from(3)]); + let _q_2 = UVKZGPoly::new(vec![ + Scalar::from(4), + Scalar::from(5), + Scalar::from(6), + Scalar::from(7), + ]); + + let y_challenge = Scalar::random(&mut rng); + + let x_challenge = Scalar::random(&mut rng); + + // Unused in this test + let u_challenge: Vec<_> = (0..num_vars).map(|_| Scalar::random(&mut rng)).collect(); + let z_challenge = Scalar::random(&mut rng); + + // Construct ζ_x using the function + let (_eval_scalar, (zeta_x_scalars, _right_quo_scalars)) = + eval_and_quotient_scalars(y_challenge, x_challenge, z_challenge, &u_challenge); + + // Now construct ζ_x explicitly + let n: u64 = 1 << num_vars; + // q_batched - \sum_k q_k * y^k * x^{N - deg(q_k) - 1} + assert_eq!(zeta_x_scalars[0], -x_challenge.pow([n - 1])); + assert_eq!( + zeta_x_scalars[1], + -y_challenge * x_challenge.pow_vartime([n - 1 - 1]) + ); + assert_eq!( + zeta_x_scalars[2], + -y_challenge * y_challenge * x_challenge.pow_vartime([n - 3 - 1]) + ); + } + + // Evaluate phi using an inefficient formula + fn phi(challenge: F, n: usize) -> F { + let length = 1 << n; + let mut result = F::ZERO; + let mut current = F::ONE; // Start with x^0 + + for _ in 0..length { + result += current; + current *= challenge; // Increment the power of x for the next iteration + } + + result + } + + #[test] + fn test_partially_evaluated_quotient_z() { + let num_vars: usize = 3; + + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + + // Define some mock q_k with deg(q_k) = 2^k - 1 + let _q_0 = UVKZGPoly::new(vec![Scalar::one()]); + let _q_1 = UVKZGPoly::new(vec![Scalar::from(2), Scalar::from(3)]); + let _q_2 = UVKZGPoly::new(vec![ + Scalar::from(4), + Scalar::from(5), + Scalar::from(6), + Scalar::from(7), + ]); + + // Unused in this test + let y_challenge = Scalar::random(&mut rng); + + let x_challenge = Scalar::random(&mut rng); + let z_challenge = Scalar::random(&mut rng); + + let u_challenge: Vec<_> = (0..num_vars).map(|_| Scalar::random(&mut rng)).collect(); + + // Construct Z_x using the function + let (_eval_scalar, (_left_quo_scalars, zeta_x_scalars)) = + eval_and_quotient_scalars(y_challenge, x_challenge, z_challenge, &u_challenge); + + // beware the Nova coefficient evaluation order! + let u_rev = { + let mut res = u_challenge.clone(); + res.reverse(); + res + }; + + // Compute Z_x directly + for k in 0..num_vars { + let x_pow_2k = x_challenge.pow([1 << k]); + let x_pow_2kp1 = x_challenge.pow([1 << (k + 1)]); + let mut scalar = + x_pow_2k * phi(x_pow_2kp1, num_vars - k - 1) - u_rev[k] * phi(x_pow_2k, num_vars - k); + scalar *= z_challenge; + scalar *= -Scalar::ONE; + assert_eq!(zeta_x_scalars[k], scalar); + } + } +} diff --git a/src/provider/util/fb_msm.rs b/src/provider/util/fb_msm.rs new file mode 100644 index 000000000..bc5b88bee --- /dev/null +++ b/src/provider/util/fb_msm.rs @@ -0,0 +1,130 @@ +/// # Fixed-base Scalar Multiplication +/// +/// This module provides an implementation of fixed-base scalar multiplication on elliptic curves. +/// +/// The multiplication is optimized through a windowed method, where scalars are broken into fixed-size +/// windows, pre-computation tables are generated, and results are efficiently combined. +use ff::{PrimeField, PrimeFieldBits}; +use group::{prime::PrimeCurve, Curve}; + +use rayon::prelude::*; + +/// Determines the window size for scalar multiplication based on the number of scalars. +/// +/// This is used to balance between pre-computation and number of point additions. +pub(crate) fn get_mul_window_size(num_scalars: usize) -> usize { + if num_scalars < 32 { + 3 + } else { + (num_scalars as f64).ln().ceil() as usize + } +} + +/// Generates a table of multiples of a base point `g` for use in windowed scalar multiplication. +/// +/// This pre-computes multiples of a base point for each window and organizes them +/// into a table for quick lookup during the scalar multiplication process. The table is a vector +/// of vectors, each inner vector corresponding to a window and containing the multiples of `g` +/// for that window. +pub(crate) fn get_window_table( + scalar_size: usize, + window: usize, + g: T, +) -> Vec> +where + T: Curve, + T::AffineRepr: Send, +{ + let in_window = 1 << window; + // Number of outer iterations needed to cover the entire scalar + let outerc = (scalar_size + window - 1) / window; + + // Number of multiples of the window's "outer point" needed for each window (fewer for the last window) + let last_in_window = 1 << (scalar_size - (outerc - 1) * window); + + let mut multiples_of_g = vec![vec![T::identity(); in_window]; outerc]; + + // Compute the multiples of g for each window + // g_outers = [ 2^{k*window}*g for k in 0..outerc] + let mut g_outer = g; + let mut g_outers = Vec::with_capacity(outerc); + for _ in 0..outerc { + g_outers.push(g_outer); + for _ in 0..window { + g_outer = g_outer.double(); + } + } + multiples_of_g + .par_iter_mut() + .enumerate() + .zip_eq(g_outers) + .for_each(|((outer, multiples_of_g), g_outer)| { + let cur_in_window = if outer == outerc - 1 { + last_in_window + } else { + in_window + }; + + // multiples_of_g = [id, g_outer, 2*g_outer, 3*g_outer, ...], + // where g_outer = 2^{outer*window}*g + let mut g_inner = T::identity(); + for inner in multiples_of_g.iter_mut().take(cur_in_window) { + *inner = g_inner; + g_inner.add_assign(&g_outer); + } + }); + multiples_of_g + .par_iter() + .map(|s| s.iter().map(|s| s.to_affine()).collect()) + .collect() +} + +/// Performs the actual windowed scalar multiplication using a pre-computed table of points. +/// +/// Given a scalar and a table of pre-computed multiples of a base point, this function +/// efficiently computes the scalar multiplication by breaking the scalar into windows and +/// adding the corresponding multiples from the table. +pub(crate) fn windowed_mul( + outerc: usize, + window: usize, + multiples_of_g: &[Vec], + scalar: &T::Scalar, +) -> T +where + T: PrimeCurve, + T::Scalar: PrimeFieldBits, +{ + let modulus_size = ::NUM_BITS as usize; + let scalar_val: Vec = scalar.to_le_bits().into_iter().collect(); + + let mut res = T::identity(); + for outer in 0..outerc { + let mut inner = 0usize; + for i in 0..window { + if outer * window + i < modulus_size && scalar_val[outer * window + i] { + inner |= 1 << i; + } + } + res.add_assign(&multiples_of_g[outer][inner]); + } + res +} + +/// Computes multiple scalar multiplications simultaneously using the windowed method. +pub(crate) fn multi_scalar_mul( + scalar_size: usize, + window: usize, + table: &[Vec], + v: &[T::Scalar], +) -> Vec +where + T: PrimeCurve, + T::Scalar: PrimeFieldBits, +{ + let outerc = (scalar_size + window - 1) / window; + assert!(outerc <= table.len()); + + v.par_iter() + .map(|e| windowed_mul::(outerc, window, table, e)) + .collect::>() +} diff --git a/src/provider/util/mod.rs b/src/provider/util/mod.rs new file mode 100644 index 000000000..43a544123 --- /dev/null +++ b/src/provider/util/mod.rs @@ -0,0 +1,2 @@ +/// Utilities for provider module +pub(crate) mod fb_msm; diff --git a/src/spartan/polys/multilinear.rs b/src/spartan/polys/multilinear.rs index 43cbe63ce..fea46c274 100644 --- a/src/spartan/polys/multilinear.rs +++ b/src/spartan/polys/multilinear.rs @@ -6,10 +6,8 @@ use std::ops::{Add, Index}; use ff::PrimeField; use itertools::Itertools as _; -use rayon::prelude::{ - IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, - IntoParallelRefMutIterator, ParallelIterator, -}; +use rand_core::{CryptoRng, RngCore}; +use rayon::prelude::*; use serde::{Deserialize, Serialize}; use crate::spartan::{math::Math, polys::eq::EqPolynomial}; @@ -47,6 +45,11 @@ impl MultilinearPolynomial { MultilinearPolynomial { num_vars, Z } } + /// evaluations of the polynomial in all the 2^num_vars Boolean inputs + pub fn evaluations(&self) -> &[Scalar] { + &self.Z[..] + } + /// Returns the number of variables in the multilinear polynomial pub const fn get_num_vars(&self) -> usize { self.num_vars @@ -57,6 +60,17 @@ impl MultilinearPolynomial { self.Z.len() } + /// Binds the polynomial's top variable using the given scalar. + /// Returns a random polynomial + /// + pub fn random(num_vars: usize, mut rng: &mut R) -> Self { + MultilinearPolynomial::new( + std::iter::from_fn(|| Some(Scalar::random(&mut rng))) + .take(1 << num_vars) + .collect(), + ) + } + /// Binds the polynomial's top variable using the given scalar. /// /// This operation modifies the polynomial in-place. @@ -180,7 +194,7 @@ mod tests { use super::*; use rand_chacha::ChaCha20Rng; - use rand_core::{CryptoRng, RngCore, SeedableRng}; + use rand_core::SeedableRng; fn make_mlp(len: usize, value: F) -> MultilinearPolynomial { MultilinearPolynomial { diff --git a/src/spartan/polys/univariate.rs b/src/spartan/polys/univariate.rs index 4bb96c5a7..ec4c0b19b 100644 --- a/src/spartan/polys/univariate.rs +++ b/src/spartan/polys/univariate.rs @@ -1,17 +1,24 @@ //! Main components: //! - `UniPoly`: an univariate dense polynomial in coefficient form (big endian), //! - `CompressedUniPoly`: a univariate dense polynomial, compressed (omitted linear term), in coefficient form (little endian), +use std::{ + cmp::Ordering, + ops::{AddAssign, Index, IndexMut, MulAssign}, +}; + use ff::PrimeField; -use rayon::prelude::{IntoParallelIterator, ParallelIterator}; +use rayon::prelude::{IntoParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; +use ref_cast::RefCast; use serde::{Deserialize, Serialize}; use crate::traits::{Group, TranscriptReprTrait}; // ax^2 + bx + c stored as vec![c, b, a] // ax^3 + bx^2 + cx + d stored as vec![d, c, b, a] -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, RefCast)] +#[repr(transparent)] pub struct UniPoly { - coeffs: Vec, + pub coeffs: Vec, } // ax^2 + bx + c stored as vec![c, a] @@ -22,6 +29,61 @@ pub struct CompressedUniPoly { } impl UniPoly { + pub fn new(coeffs: Vec) -> Self { + let mut res = UniPoly { coeffs }; + res.truncate_leading_zeros(); + res + } + + fn zero() -> Self { + UniPoly::new(Vec::new()) + } + + /// Divide self by another polynomial, and returns the + /// quotient and remainder. + pub fn divide_with_q_and_r(&self, divisor: &Self) -> Option<(UniPoly, UniPoly)> { + if self.is_zero() { + Some((UniPoly::zero(), UniPoly::zero())) + } else if divisor.is_zero() { + panic!("Dividing by zero polynomial") + } else if self.degree() < divisor.degree() { + Some((UniPoly::zero(), self.clone())) + } else { + // Now we know that self.degree() >= divisor.degree(); + let mut quotient = vec![Scalar::ZERO; self.degree() - divisor.degree() + 1]; + let mut remainder: UniPoly = self.clone(); + // Can unwrap here because we know self is not zero. + let divisor_leading_inv = divisor.leading_coefficient().unwrap().invert().unwrap(); + while !remainder.is_zero() && remainder.degree() >= divisor.degree() { + let cur_q_coeff = *remainder.leading_coefficient().unwrap() * divisor_leading_inv; + let cur_q_degree = remainder.degree() - divisor.degree(); + quotient[cur_q_degree] = cur_q_coeff; + + for (i, div_coeff) in divisor.coeffs.iter().enumerate() { + remainder.coeffs[cur_q_degree + i] -= &(cur_q_coeff * div_coeff); + } + while let Some(true) = remainder.coeffs.last().map(|c| c == &Scalar::ZERO) { + remainder.coeffs.pop(); + } + } + Some((UniPoly::new(quotient), remainder)) + } + } + + pub fn is_zero(&self) -> bool { + self.coeffs.is_empty() || self.coeffs.iter().all(|c| c == &Scalar::ZERO) + } + + fn truncate_leading_zeros(&mut self) { + while self.coeffs.last().map_or(false, |c| c == &Scalar::ZERO) { + self.coeffs.pop(); + } + } + + pub fn leading_coefficient(&self) -> Option<&Scalar> { + self.coeffs.last() + } + pub fn from_evals(evals: &[Scalar]) -> Self { // we only support degree-2 or degree-3 univariate polynomials assert!(evals.len() == 3 || evals.len() == 4); @@ -115,11 +177,61 @@ impl TranscriptReprTrait for UniPoly { .collect::>() } } + +impl Index for UniPoly { + type Output = Scalar; + + fn index(&self, index: usize) -> &Self::Output { + &self.coeffs[index] + } +} + +impl IndexMut for UniPoly { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.coeffs[index] + } +} + +impl AddAssign<&Scalar> for UniPoly { + fn add_assign(&mut self, rhs: &Scalar) { + self.coeffs.par_iter_mut().for_each(|c| *c += rhs); + } +} + +impl MulAssign<&Scalar> for UniPoly { + fn mul_assign(&mut self, rhs: &Scalar) { + self.coeffs.par_iter_mut().for_each(|c| *c *= rhs); + } +} + +impl AddAssign<&Self> for UniPoly { + fn add_assign(&mut self, rhs: &Self) { + let ordering = self.coeffs.len().cmp(&rhs.coeffs.len()); + #[allow(clippy::disallowed_methods)] + for (lhs, rhs) in self.coeffs.iter_mut().zip(&rhs.coeffs) { + *lhs += rhs; + } + if matches!(ordering, Ordering::Less) { + self + .coeffs + .extend(rhs.coeffs[self.coeffs.len()..].iter().cloned()); + } + if matches!(ordering, Ordering::Equal) { + self.truncate_leading_zeros(); + } + } +} + +impl AsRef> for UniPoly { + fn as_ref(&self) -> &Vec { + &self.coeffs + } +} + #[cfg(test)] mod tests { - use crate::provider::{bn256_grumpkin, secp_secq::secp256k1}; - use super::*; + use crate::provider::{bn256_grumpkin, secp_secq::secp256k1}; fn test_from_evals_quad_with() { // polynomial is 2x^2 + 3x + 1