diff --git a/Cargo.toml b/Cargo.toml index 77e147131..820ca43d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ byteorder = "1.4.3" thiserror = "1.0" halo2curves = { version = "0.4.0", features = ["derive_serde"] } group = "0.13.0" +pairing = "0.23.0" abomonation = "0.7.3" abomonation_derive = { version = "0.1.0", package = "abomonation_derive_ng" } tap = "1.0.1" @@ -43,6 +44,8 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } cfg-if = "1.0.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 = { git="https://github.com/lurk-lab/pasta-msm", branch="dev", version = "0.1.4" } @@ -59,7 +62,6 @@ pprof = { version = "0.11" } sha2 = "0.10.7" tracing-test = "0.2.4" proptest = "1.2.0" -rand = "0.8.5" [[bench]] name = "recursive-snark" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 5e91e6179..008fe1d5b 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -52,3 +52,22 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------ +https://github.com/AztecProtocol/aztec-packages/ + +Licensed under Apache 2.0 + +Copyright 2022 Aztec + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs index 121704b9e..b203cbcc5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -36,9 +36,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, @@ -70,3 +70,17 @@ pub enum NovaError { #[error("InternalError")] InternalError, } + +/// 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 e204bfa12..21b2bceb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1000,14 +1000,15 @@ mod tests { use super::*; use crate::{ provider::{ - traits::DlogGroup, Bn256Engine, GrumpkinEngine, PallasEngine, Secp256k1Engine, - Secq256k1Engine, VestaEngine, + non_hiding_zeromorph::ZMPCS, traits::DlogGroup, Bn256Engine, Bn256EngineZM, GrumpkinEngine, + PallasEngine, Secp256k1Engine, Secq256k1Engine, VestaEngine, }, traits::{evaluation::EvaluationEngineTrait, snark::default_ck_hint}, }; use ::bellpepper_core::{num::AllocatedNum, ConstraintSystem, SynthesisError}; use core::{fmt::Write, marker::PhantomData}; use ff::PrimeField; + use halo2curves::bn256::Bn256; use traits::circuit::TrivialCircuit; type EE = provider::ipa_pc::EvaluationEngine; @@ -1142,6 +1143,18 @@ mod tests { &trivial_circuit2_grumpkin, "5aec6defcb0f6b2bb14aec70362419388916d7a5bc528c0b3fabb197ae57cb03", ); + #[cfg(not(feature = "asm"))] + test_pp_digest_with::, EE<_>>( + &trivial_circuit1_grumpkin, + &trivial_circuit2_grumpkin, + "e20ab87e395e787e272330a4c9c79916b83bf95632d9604511ad6a1448625402", + ); + #[cfg(not(feature = "asm"))] + test_pp_digest_with::, EE<_>>( + &cubic_circuit1_grumpkin, + &trivial_circuit2_grumpkin, + "ce304ffff7f6dfc143322bb621e9025ba9d77f1b8d0e199e6bc432aa66c98101", + ); let trivial_circuit1_secp = TrivialCircuit::<::Scalar>::default(); let trivial_circuit2_secp = TrivialCircuit::<::Scalar>::default(); @@ -1384,6 +1397,12 @@ mod tests { test_ivc_nontrivial_with_compression_with::, EE<_>>(); test_ivc_nontrivial_with_compression_with::, EE<_>>(); test_ivc_nontrivial_with_compression_with::, EE<_>>(); + test_ivc_nontrivial_with_compression_with::< + Bn256EngineZM, + GrumpkinEngine, + ZMPCS, + EE<_>, + >(); } fn test_ivc_nontrivial_with_spark_compression_with() @@ -1484,6 +1503,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() @@ -1626,6 +1651,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 116c44dda..80cb66386 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::{ @@ -406,7 +406,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..f1e6fa5ab --- /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 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 badc1caa3..1ba2e27b6 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -2,6 +2,7 @@ // public modules to be used as an evaluation engine with Spartan pub mod ipa_pc; +pub mod non_hiding_zeromorph; // crate-public modules, made crate-public mostly for tests pub(crate) mod bn256_grumpkin; @@ -10,6 +11,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..ff6bb57a4 --- /dev/null +++ b/src/provider/non_hiding_kzg.rs @@ -0,0 +1,357 @@ +//! Non-hiding variant of KZG10 scheme for univariate polynomials. +use abomonation_derive::Abomonation; +use ff::{Field, PrimeField, PrimeFieldBits}; +use group::{prime::PrimeCurveAffine, Curve, Group as _}; +use 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, Abomonation)] +#[serde(bound( + serialize = "E::G1Affine: Serialize, E::G2Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" +))] +#[abomonation_omit_bounds] +pub struct UVUniversalKZGParam { + /// Group elements of the form `{ β^i G }`, where `i` ranges from 0 to + /// `degree`. + #[abomonate_with(Vec<[u64; 8]>)] // // this is a hack; we just assume the size of the element. + pub powers_of_g: Vec, + /// Group elements of the form `{ β^i H }`, where `i` ranges from 0 to + /// `degree`. + #[abomonate_with(Vec<[u64; 16]>)] // this is a hack; we just assume the size of the element. + 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, Abomonation)] +#[abomonation_omit_bounds] +#[serde(bound( + serialize = "E::G1Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>" +))] +pub struct UVKZGProverKey { + /// generators + #[abomonate_with(Vec<[u64; 8]>)] // this is a hack; we just assume the size of the element. + pub powers_of_g: Vec, +} + +/// `UVKZGVerifierKey` is used to check evaluation proofs for a given +/// commitment. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Abomonation)] +#[abomonation_omit_bounds] +#[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. + #[abomonate_with([u64; 8])] // this is a hack; we just assume the size of the element. + pub g: E::G1Affine, + /// The generator of G2. + #[abomonate_with([u64; 16])] // this is a hack; we just assume the size of the element. + pub h: E::G2Affine, + /// β times the above generator of G2. + #[abomonate_with([u64; 16])] // this is a hack; we just assume the size of the element. + 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..7f9de2e73 --- /dev/null +++ b/src/provider/non_hiding_zeromorph.rs @@ -0,0 +1,804 @@ +//! 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 abomonation_derive::Abomonation; +use ff::{BatchInvert, Field, PrimeField, PrimeFieldBits}; +use group::{Curve, Group as _}; +use itertools::Itertools as _; +use 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, Abomonation)] +#[abomonation_omit_bounds] +#[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, Abomonation)] +#[abomonation_omit_bounds] +#[serde(bound( + serialize = "E::G1Affine: Serialize, E::G2Affine: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" +))] +pub struct ZMVerifierKey { + vp: UVKZGVerifierKey, + #[abomonate_with([u64; 16])] // this is a hack; we just assume the size of the element. + 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 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 b564fdb02..20f1d54f6 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}; @@ -48,6 +46,11 @@ impl MultilinearPolynomial { } } + /// 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 @@ -58,6 +61,16 @@ impl MultilinearPolynomial { self.Z.len() } + /// 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(), + ) + } + /// Bounds the polynomial's top variable using the given scalar. /// /// This operation modifies the polynomial in-place. @@ -186,7 +199,7 @@ mod tests { use super::*; use pasta_curves::Fp; use rand_chacha::ChaCha20Rng; - use rand_core::{CryptoRng, RngCore, SeedableRng}; + use rand_core::SeedableRng; fn make_mlp(len: usize, value: F) -> MultilinearPolynomial { MultilinearPolynomial { @@ -266,7 +279,7 @@ mod tests { let num_evals = 4; let mut evals: Vec = Vec::with_capacity(num_evals); for _ in 0..num_evals { - evals.push(F::from_u128(8)); + evals.push(F::from(8)); } let dense_poly: MultilinearPolynomial = MultilinearPolynomial::new(evals.clone()); @@ -295,18 +308,6 @@ mod tests { test_evaluation_with::(); } - /// Returns a random ML polynomial - fn random( - num_vars: usize, - mut rng: &mut R, - ) -> MultilinearPolynomial { - MultilinearPolynomial::new( - std::iter::from_fn(|| Some(Scalar::random(&mut rng))) - .take(1 << num_vars) - .collect(), - ) - } - /// This evaluates a multilinear polynomial at a partial point in the evaluation domain, /// which forces us to model how we pass coordinates to the evaluation function precisely. fn partial_eval( @@ -351,7 +352,7 @@ mod tests { // Initialize a random polynomial let n = 5; let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let poly = random(n, &mut rng); + let poly = MultilinearPolynomial::random(n, &mut rng); // Define a random multivariate evaluation point u = (u_0, u_1, u_2, u_3, u_4) let u_0 = F::random(&mut rng); @@ -391,7 +392,7 @@ mod tests { // Initialize a random polynomial let n = 7; let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let poly = random(n, &mut rng); + let poly = MultilinearPolynomial::random(n, &mut rng); // draw a random point let pt: Vec<_> = std::iter::from_fn(|| Some(F::random(&mut rng))) diff --git a/src/spartan/polys/univariate.rs b/src/spartan/polys/univariate.rs index bfe983e5b..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)] +#[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