From 53deada5596af06c4e76d215cfdb7b4eb0f204bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= <4142+huitseeker@users.noreply.github.com> Date: Thu, 2 May 2024 09:55:00 +0200 Subject: [PATCH 1/3] Maintenance (#381) * chore: bump rust version * Improve sparse polynomial evaluation algorithm (#317) * time-optimal algorithm for sparse polynomial evaluation * update version --------- Co-authored-by: Srinath Setty --- rust-toolchain.toml | 2 +- src/bellpepper/test_shape_cs.rs | 1 + src/spartan/batched.rs | 12 +----- src/spartan/batched_ppsnark.rs | 10 +---- src/spartan/mod.rs | 1 - src/spartan/polys/multilinear.rs | 74 ++++++++++++++------------------ src/spartan/ppsnark.rs | 24 +++++------ src/spartan/snark.rs | 19 +++----- 8 files changed, 55 insertions(+), 88 deletions(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a58a147f9..36d6d232b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # The default profile includes rustc, rust-std, cargo, rust-docs, rustfmt and clippy. profile = "default" -channel = "1.76.0" +channel = "1.77" targets = [ "wasm32-unknown-unknown" ] diff --git a/src/bellpepper/test_shape_cs.rs b/src/bellpepper/test_shape_cs.rs index 7193b885f..03d209509 100644 --- a/src/bellpepper/test_shape_cs.rs +++ b/src/bellpepper/test_shape_cs.rs @@ -14,6 +14,7 @@ use ff::{Field, PrimeField}; #[derive(Clone, Copy)] struct OrderedVariable(Variable); +#[allow(dead_code)] #[derive(Debug)] enum NamedObject { Constraint(usize), diff --git a/src/spartan/batched.rs b/src/spartan/batched.rs index 3604c1282..b68ac33c6 100644 --- a/src/spartan/batched.rs +++ b/src/spartan/batched.rs @@ -490,17 +490,7 @@ impl> BatchedRelaxedR1CSSNARKTrait let evals_Z = zip_with!(iter, (self.evals_W, U, r_y), |eval_W, U, r_y| { let eval_X = { // constant term - let poly_X = iter::once((0, U.u)) - .chain( - //remaining inputs - U.X - .iter() - .enumerate() - // filter_map uses the sparsity of the polynomial, if irrelevant - // we should replace by UniPoly - .filter_map(|(i, x_i)| (!x_i.is_zero_vartime()).then_some((i + 1, *x_i))), - ) - .collect(); + let poly_X = iter::once(U.u).chain(U.X.iter().cloned()).collect(); SparsePolynomial::new(r_y.len() - 1, poly_X).evaluate(&r_y[1..]) }; (E::Scalar::ONE - r_y[0]) * eval_W + r_y[0] * eval_X diff --git a/src/spartan/batched_ppsnark.rs b/src/spartan/batched_ppsnark.rs index f60e562e1..caf604804 100644 --- a/src/spartan/batched_ppsnark.rs +++ b/src/spartan/batched_ppsnark.rs @@ -927,15 +927,7 @@ impl> BatchedRelaxedR1CSSNARKTrait let X = { // constant term - let poly_X = std::iter::once((0, U.u)) - .chain( - //remaining inputs - (0..U.X.len()) - // filter_map uses the sparsity of the polynomial, if irrelevant - // we should replace by UniPoly - .filter_map(|i| (!U.X[i].is_zero_vartime()).then_some((i + 1, U.X[i]))), - ) - .collect(); + let poly_X = std::iter::once(U.u).chain(U.X.iter().cloned()).collect(); SparsePolynomial::new(num_vars_log, poly_X).evaluate(&rand_sc_unpad[1..]) }; diff --git a/src/spartan/mod.rs b/src/spartan/mod.rs index 663addb98..ff17677c8 100644 --- a/src/spartan/mod.rs +++ b/src/spartan/mod.rs @@ -23,7 +23,6 @@ use crate::{ }; use ff::Field; use itertools::Itertools as _; -use polys::multilinear::SparsePolynomial; use rayon::{iter::IntoParallelRefIterator, prelude::*}; use rayon_scan::ScanParallelIterator as _; use ref_cast::RefCast; diff --git a/src/spartan/polys/multilinear.rs b/src/spartan/polys/multilinear.rs index bdb18e06f..a4817ccdf 100644 --- a/src/spartan/polys/multilinear.rs +++ b/src/spartan/polys/multilinear.rs @@ -8,8 +8,7 @@ use ff::PrimeField; use itertools::Itertools as _; use rand_core::{CryptoRng, RngCore}; use rayon::prelude::{ - IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, - IntoParallelRefMutIterator, ParallelIterator, + IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator, }; use serde::{Deserialize, Serialize}; @@ -130,47 +129,37 @@ impl Index for MultilinearPolynomial { } /// Sparse multilinear polynomial, which means the $Z(\cdot)$ is zero at most points. -/// So we do not have to store every evaluations of $Z(\cdot)$, only store the non-zero points. -/// -/// For example, the evaluations are [0, 0, 0, 1, 0, 1, 0, 2]. -/// The sparse polynomial only store the non-zero values, [(3, 1), (5, 1), (7, 2)]. -/// In the tuple, the first is index, the second is value. +/// In our context, sparse polynomials are non-zeros over the hypercube at locations that map to "small" integers +/// We exploit this property to implement a time-optimal algorithm pub(crate) struct SparsePolynomial { num_vars: usize, - Z: Vec<(usize, Scalar)>, + Z: Vec, } impl SparsePolynomial { - pub fn new(num_vars: usize, Z: Vec<(usize, Scalar)>) -> Self { + pub fn new(num_vars: usize, Z: Vec) -> Self { Self { num_vars, Z } } - /// Computes the $\tilde{eq}$ extension polynomial. - /// return 1 when a == r, otherwise return 0. - fn compute_chi(a: &[bool], r: &[Scalar]) -> Scalar { - assert_eq!(a.len(), r.len()); - let mut chi_i = Scalar::ONE; - for j in 0..r.len() { - if a[j] { - chi_i *= r[j]; - } else { - chi_i *= Scalar::ONE - r[j]; - } - } - chi_i - } - - // Takes O(m log n) where m is the number of non-zero evaluations and n is the number of variables. + // a time-optimal algorithm to evaluate sparse polynomials pub fn evaluate(&self, r: &[Scalar]) -> Scalar { assert_eq!(self.num_vars, r.len()); - (0..self.Z.len()) - .into_par_iter() - .map(|i| { - let bits = (self.Z[i].0).get_bits(r.len()); - Self::compute_chi(&bits, r) * self.Z[i].1 - }) - .sum() + let num_vars_z = self.Z.len().next_power_of_two().log_2(); + let chis = EqPolynomial::evals_from_points(&r[self.num_vars - 1 - num_vars_z..]); + #[allow(clippy::disallowed_methods)] + let eval_partial: Scalar = self + .Z + .iter() + .zip(chis.iter()) + .map(|(z, chi)| *z * *chi) + .sum(); + + let common = (0..self.num_vars - 1 - num_vars_z) + .map(|i| (Scalar::ONE - r[i])) + .product::(); + + common * eval_partial } } @@ -232,18 +221,21 @@ mod tests { } fn test_sparse_polynomial_with() { - // Let the polynomial have 3 variables, p(x_1, x_2, x_3) = (x_1 + x_2) * x_3 - // Evaluations of the polynomial at boolean cube are [0, 0, 0, 1, 0, 1, 0, 2]. + // Let the polynomial have 4 variables, but is non-zero at only 3 locations (out of 2^4 = 16) over the hypercube + let mut Z = vec![F::ONE, F::ONE, F::from(2)]; + let m_poly = SparsePolynomial::::new(4, Z.clone()); - let TWO = F::from(2); - let Z = vec![(3, F::ONE), (5, F::ONE), (7, TWO)]; - let m_poly = SparsePolynomial::::new(3, Z); + Z.resize(16, F::ZERO); // append with zeros to make it a dense polynomial + let m_poly_dense = MultilinearPolynomial::new(Z); - let x = vec![F::ONE, F::ONE, F::ONE]; - assert_eq!(m_poly.evaluate(x.as_slice()), TWO); + // evaluation point + let x = vec![F::from(5), F::from(8), F::from(5), F::from(3)]; - let x = vec![F::ONE, F::ZERO, F::ONE]; - assert_eq!(m_poly.evaluate(x.as_slice()), F::ONE); + // check evaluations + assert_eq!( + m_poly.evaluate(x.as_slice()), + m_poly_dense.evaluate(x.as_slice()) + ); } #[test] diff --git a/src/spartan/ppsnark.rs b/src/spartan/ppsnark.rs index aba5a98be..5cc723c34 100644 --- a/src/spartan/ppsnark.rs +++ b/src/spartan/ppsnark.rs @@ -24,7 +24,7 @@ use crate::{ }, SumcheckProof, }, - PolyEvalInstance, PolyEvalWitness, SparsePolynomial, + PolyEvalInstance, PolyEvalWitness, }, traits::{ commitment::{CommitmentEngineTrait, CommitmentTrait, Len}, @@ -42,7 +42,7 @@ use rayon::prelude::*; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use super::polys::masked_eq::MaskedEqPolynomial; +use super::polys::{masked_eq::MaskedEqPolynomial, multilinear::SparsePolynomial}; fn padded(v: &[E::Scalar], n: usize, e: &E::Scalar) -> Vec { let mut v_padded = vec![*e; n]; @@ -930,17 +930,15 @@ impl> RelaxedR1CSSNARKTrait for Relax }; let eval_X = { - // constant term - let poly_X = std::iter::once((0, U.u)) - .chain( - //remaining inputs - (0..U.X.len()) - // filter_map uses the sparsity of the polynomial, if irrelevant - // we should replace by UniPoly - .filter_map(|i| (!U.X[i].is_zero_vartime()).then_some((i + 1, U.X[i]))), - ) - .collect(); - SparsePolynomial::new(vk.num_vars.log_2(), poly_X).evaluate(&rand_sc_unpad[1..]) + // public IO is (u, X) + let X = vec![U.u] + .into_iter() + .chain(U.X.iter().cloned()) + .collect::>(); + + // evaluate the sparse polynomial at rand_sc_unpad[1..] + let poly_X = SparsePolynomial::new(rand_sc_unpad.len() - 1, X); + poly_X.evaluate(&rand_sc_unpad[1..]) }; self.eval_W + factor * rand_sc_unpad[0] * eval_X diff --git a/src/spartan/snark.rs b/src/spartan/snark.rs index 636a0d7d8..538647e21 100644 --- a/src/spartan/snark.rs +++ b/src/spartan/snark.rs @@ -32,7 +32,7 @@ use itertools::Itertools as _; use once_cell::sync::OnceCell; use rayon::prelude::*; use serde::{Deserialize, Serialize}; -use std::{iter, sync::Arc}; +use std::sync::Arc; /// A type that represents the prover's key #[derive(Debug, Clone)] @@ -328,17 +328,12 @@ impl> RelaxedR1CSSNARKTrait for Relax // verify claim_inner_final let eval_Z = { let eval_X = { - // constant term - let poly_X = iter::once((0, U.u)) - .chain( - //remaining inputs - (0..U.X.len()) - // filter_map uses the sparsity of the polynomial, if irrelevant - // we should replace by UniPoly - .filter_map(|i| (!U.X[i].is_zero_vartime()).then_some((i + 1, U.X[i]))), - ) - .collect(); - SparsePolynomial::new(usize::try_from(vk.S.num_vars.ilog2()).unwrap(), poly_X) + // public IO is (u, X) + let X = vec![U.u] + .into_iter() + .chain(U.X.iter().cloned()) + .collect::>(); + SparsePolynomial::new(usize::try_from(vk.S.num_vars.ilog2()).unwrap(), X) .evaluate(&r_y[1..]) }; (E::Scalar::ONE - r_y[0]) * self.eval_W + r_y[0] * eval_X From f3d7790618728a0c337ac3ca8300cd85568c39d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 13:14:08 -0400 Subject: [PATCH 2/3] chore(deps): update itertools requirement in the rust-dependencies group (#385) Updates the requirements on [itertools](https://github.com/rust-itertools/itertools) to permit the latest version. Updates `itertools` to 0.13.0 - [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-itertools/itertools/compare/v0.12.0...v0.13.0) --- updated-dependencies: - dependency-name: itertools dependency-type: direct:production dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d86b2b4f5..878c9cd0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ abomonation_derive = { version = "0.1.0", package = "abomonation_derive_ng" } tracing = "0.1.37" cfg-if = "1.0.0" once_cell = "1.18.0" -itertools = "0.12.0" # zip_eq +itertools = "0.13.0" # zip_eq rand = "0.8.5" ref-cast = "1.0.20" # allocation-less conversion in multilinear polys derive_more = "0.99.17" # lightens impl macros for pasta From 3043e8cc673617572c21b44a11c88e6965e9cbe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= <4142+huitseeker@users.noreply.github.com> Date: Wed, 12 Jun 2024 08:30:38 -0400 Subject: [PATCH 3/3] refactor: Bump toolchain and clean lints (#387) - Upgraded the Rust toolchain version. - clippy --- examples/hashchain.rs | 4 ++-- rust-toolchain.toml | 2 +- src/gadgets/nonnative/mod.rs | 4 ---- src/provider/poseidon.rs | 4 ++-- src/provider/tests/mod.rs | 2 +- src/provider/util/mod.rs | 2 +- src/spartan/math.rs | 15 --------------- src/spartan/polys/multilinear.rs | 6 +++--- src/supernova/mod.rs | 13 ------------- 9 files changed, 10 insertions(+), 42 deletions(-) diff --git a/examples/hashchain.rs b/examples/hashchain.rs index 3f6ffdb42..10c0383ec 100644 --- a/examples/hashchain.rs +++ b/examples/hashchain.rs @@ -92,9 +92,9 @@ impl StepCircuit for HashChainCircuit { let acc = &mut ns; sponge.start(parameter, None, acc); - neptune::sponge::api::SpongeAPI::absorb(&mut sponge, num_absorbs, &elt, acc); + SpongeAPI::absorb(&mut sponge, num_absorbs, &elt, acc); - let output = neptune::sponge::api::SpongeAPI::squeeze(&mut sponge, 1, acc); + let output = SpongeAPI::squeeze(&mut sponge, 1, acc); sponge.finish(acc).unwrap(); Elt::ensure_allocated(&output[0], &mut ns.namespace(|| "ensure allocated"), true)? }; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 36d6d232b..e4205e80b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # The default profile includes rustc, rust-std, cargo, rust-docs, rustfmt and clippy. profile = "default" -channel = "1.77" +channel = "1.78" targets = [ "wasm32-unknown-unknown" ] diff --git a/src/gadgets/nonnative/mod.rs b/src/gadgets/nonnative/mod.rs index 8167e5a7e..4d611cbb0 100644 --- a/src/gadgets/nonnative/mod.rs +++ b/src/gadgets/nonnative/mod.rs @@ -6,16 +6,12 @@ use ff::PrimeField; trait OptionExt { fn grab(&self) -> Result<&T, SynthesisError>; - fn grab_mut(&mut self) -> Result<&mut T, SynthesisError>; } impl OptionExt for Option { fn grab(&self) -> Result<&T, SynthesisError> { self.as_ref().ok_or(SynthesisError::AssignmentMissing) } - fn grab_mut(&mut self) -> Result<&mut T, SynthesisError> { - self.as_mut().ok_or(SynthesisError::AssignmentMissing) - } } trait BitAccess { diff --git a/src/provider/poseidon.rs b/src/provider/poseidon.rs index 3a42bf4a9..de4e8b298 100644 --- a/src/provider/poseidon.rs +++ b/src/provider/poseidon.rs @@ -169,7 +169,7 @@ where assert_eq!(self.num_absorbs, self.state.len()); sponge.start(parameter, None, acc); - neptune::sponge::api::SpongeAPI::absorb( + SpongeAPI::absorb( &mut sponge, self.num_absorbs as u32, &(0..self.state.len()) @@ -178,7 +178,7 @@ where acc, ); - let output = neptune::sponge::api::SpongeAPI::squeeze(&mut sponge, 1, acc); + let output = SpongeAPI::squeeze(&mut sponge, 1, acc); sponge.finish(acc).unwrap(); output }; diff --git a/src/provider/tests/mod.rs b/src/provider/tests/mod.rs index 6f2858161..7a473c50f 100644 --- a/src/provider/tests/mod.rs +++ b/src/provider/tests/mod.rs @@ -25,7 +25,7 @@ pub mod solidity_compatibility_utils { ) { use rand_core::SeedableRng; - let mut rng = rand::rngs::StdRng::seed_from_u64(num_vars as u64); + let mut rng = StdRng::seed_from_u64(num_vars as u64); let (poly, point, eval) = crate::provider::util::test_utils::random_poly_with_eval::(num_vars, &mut rng); diff --git a/src/provider/util/mod.rs b/src/provider/util/mod.rs index bff8340f1..44eabf14d 100644 --- a/src/provider/util/mod.rs +++ b/src/provider/util/mod.rs @@ -146,7 +146,7 @@ pub mod test_utils { ) { use rand_core::SeedableRng; - let mut rng = rand::rngs::StdRng::seed_from_u64(num_vars as u64); + let mut rng = StdRng::seed_from_u64(num_vars as u64); let (poly, point, eval) = random_poly_with_eval::(num_vars, &mut rng); diff --git a/src/spartan/math.rs b/src/spartan/math.rs index 4a879b93d..94b35c50f 100644 --- a/src/spartan/math.rs +++ b/src/spartan/math.rs @@ -1,23 +1,8 @@ pub trait Math { - fn pow2(self) -> usize; - fn get_bits(self, num_bits: usize) -> Vec; fn log_2(self) -> usize; } impl Math for usize { - #[inline] - fn pow2(self) -> usize { - let base: Self = 2; - base.pow(self as u32) - } - - /// Returns the `num_bits` from n in a canonical order - fn get_bits(self, num_bits: usize) -> Vec { - (0..num_bits) - .map(|shift_amount| ((self & (1 << (num_bits - shift_amount - 1))) > 0)) - .collect::>() - } - fn log_2(self) -> usize { assert_ne!(self, 0); diff --git a/src/spartan/polys/multilinear.rs b/src/spartan/polys/multilinear.rs index a4817ccdf..7ce2047c8 100644 --- a/src/spartan/polys/multilinear.rs +++ b/src/spartan/polys/multilinear.rs @@ -181,7 +181,7 @@ impl Add for MultilinearPolynomial { #[cfg(test)] mod tests { - use crate::provider::{self, bn256_grumpkin::bn256, secp_secq::secp256k1}; + use crate::provider::{bn256_grumpkin::bn256, secp_secq::secp256k1}; use super::*; use rand_chacha::ChaCha20Rng; @@ -293,8 +293,8 @@ mod tests { #[test] fn test_evaluation() { test_evaluation_with::(); - test_evaluation_with::(); - test_evaluation_with::(); + test_evaluation_with::(); + test_evaluation_with::(); } /// This binds the variables of a multilinear polynomial to a provided sequence diff --git a/src/supernova/mod.rs b/src/supernova/mod.rs index 92df409c0..347f2c25a 100644 --- a/src/supernova/mod.rs +++ b/src/supernova/mod.rs @@ -1155,19 +1155,6 @@ where fn secondary_circuit(&self) -> Self::C2; } -/// Extension trait to simplify getting scalar form of initial circuit index. -trait InitialProgramCounter: NonUniformCircuit -where - E1: CurveCycleEquipped, -{ - /// Initial program counter is the initial circuit index as a `Scalar`. - fn initial_program_counter(&self) -> E1::Scalar { - E1::Scalar::from(self.initial_circuit_index() as u64) - } -} - -impl> InitialProgramCounter for T {} - /// Compute the circuit digest of a supernova [`StepCircuit`]. /// /// Note for callers: This function should be called with its performance characteristics in mind.