diff --git a/Cargo.toml b/Cargo.toml index 379b00a13..26dc0b98d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nova-snark" -version = "0.22.0" +version = "0.23.0" authors = ["Srinath Setty "] edition = "2021" description = "Recursive zkSNARKs without trusted setup" @@ -28,11 +28,11 @@ num-traits = "0.2" num-integer = "0.1" serde = { version = "1.0", features = ["derive"] } bincode = "1.3" -flate2 = "1.0" bitvec = "1.0" byteorder = "1.4.3" -thiserror = "1.0" -halo2curves = { version = "0.1.0", features = ["derive_serde"] } +thiserror = "1.0" +rand = "0.8.4" +halo2curves = { version="0.1.0", features = [ "derive_serde" ] } [target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] pasta-msm = { version = "0.1.4" } @@ -44,10 +44,12 @@ getrandom = { version = "0.2.0", default-features = false, features = ["js"] } [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } rand = "0.8.4" +flate2 = "1.0" hex = "0.4.3" pprof = { version = "0.11" } cfg-if = "1.0.0" sha2 = "0.10.7" +proptest = "1.2.0" [[bench]] name = "recursive-snark" @@ -66,9 +68,11 @@ name = "sha256" harness = false [features] -default = [] +default = ["hypernova"] # Compiles in portable mode, w/o ISA extensions => binary can be executed on all systems. portable = ["pasta-msm/portable"] cuda = ["neptune/cuda", "neptune/pasta", "neptune/arity24"] opencl = ["neptune/opencl", "neptune/pasta", "neptune/arity24"] +hypernova = [] + flamegraph = ["pprof/flamegraph", "pprof/criterion"] diff --git a/README.md b/README.md index feca29336..84d6a41d5 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,15 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### Experimental features + +To run early experimental work on CCS and HyperNova, use the `hypernova` feature flag: + +```text +cargo test --features hypernova ccs +``` + ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft diff --git a/benches/compressed-snark.rs b/benches/compressed-snark.rs index 4effe2620..2402217e2 100644 --- a/benches/compressed-snark.rs +++ b/benches/compressed-snark.rs @@ -17,8 +17,12 @@ type G1 = pasta_curves::pallas::Point; type G2 = pasta_curves::vesta::Point; type EE1 = nova_snark::provider::ipa_pc::EvaluationEngine; type EE2 = nova_snark::provider::ipa_pc::EvaluationEngine; +// SNARKs without computational commitments type S1 = nova_snark::spartan::snark::RelaxedR1CSSNARK; type S2 = nova_snark::spartan::snark::RelaxedR1CSSNARK; +// SNARKs with computational commitments +type SS1 = nova_snark::spartan::ppsnark::RelaxedR1CSSNARK; +type SS2 = nova_snark::spartan::ppsnark::RelaxedR1CSSNARK; type C1 = NonTrivialTestCircuit<::Scalar>; type C2 = TrivialTestCircuit<::Scalar>; @@ -31,13 +35,13 @@ cfg_if::cfg_if! { criterion_group! { name = compressed_snark; config = Criterion::default().warm_up_time(Duration::from_millis(3000)).with_profiler(pprof::criterion::PProfProfiler::new(100, pprof::criterion::Output::Flamegraph(None))); - targets = bench_compressed_snark + targets = bench_compressed_snark, bench_compressed_snark_with_computational_commitments } } else { criterion_group! { name = compressed_snark; config = Criterion::default().warm_up_time(Duration::from_millis(3000)); - targets = bench_compressed_snark + targets = bench_compressed_snark, bench_compressed_snark_with_computational_commitments } } } @@ -61,7 +65,7 @@ fn bench_compressed_snark(c: &mut Criterion) { let c_secondary = TrivialTestCircuit::default(); // Produce public parameters - let pp = PublicParams::::setup(c_primary.clone(), c_secondary.clone()); + let pp = PublicParams::::setup(&c_primary, &c_secondary); // Produce prover and verifier keys for CompressedSNARK let (pk, vk) = CompressedSNARK::<_, _, _, _, S1, S2>::setup(&pp).unwrap(); @@ -129,6 +133,93 @@ fn bench_compressed_snark(c: &mut Criterion) { } } +fn bench_compressed_snark_with_computational_commitments(c: &mut Criterion) { + let num_samples = 10; + let num_cons_verifier_circuit_primary = 9819; + // we vary the number of constraints in the step circuit + for &num_cons_in_augmented_circuit in [9819, 16384, 32768, 65536, 131072, 262144].iter() { + // number of constraints in the step circuit + let num_cons = num_cons_in_augmented_circuit - num_cons_verifier_circuit_primary; + + let mut group = c.benchmark_group(format!( + "CompressedSNARK-Commitments-StepCircuitSize-{num_cons}" + )); + group + .sampling_mode(SamplingMode::Flat) + .sample_size(num_samples); + + let c_primary = NonTrivialTestCircuit::new(num_cons); + let c_secondary = TrivialTestCircuit::default(); + + // Produce public parameters + let pp = PublicParams::::setup(&c_primary, &c_secondary); + + // Produce prover and verifier keys for CompressedSNARK + let (pk, vk) = CompressedSNARK::<_, _, _, _, SS1, SS2>::setup(&pp).unwrap(); + + // produce a recursive SNARK + let num_steps = 3; + let mut recursive_snark: RecursiveSNARK = RecursiveSNARK::new( + &pp, + &c_primary, + &c_secondary, + vec![::Scalar::from(2u64)], + vec![::Scalar::from(2u64)], + ); + + for i in 0..num_steps { + let res = recursive_snark.prove_step( + &pp, + &c_primary, + &c_secondary, + vec![::Scalar::from(2u64)], + vec![::Scalar::from(2u64)], + ); + assert!(res.is_ok()); + + // verify the recursive snark at each step of recursion + let res = recursive_snark.verify( + &pp, + i + 1, + &[::Scalar::from(2u64)], + &[::Scalar::from(2u64)], + ); + assert!(res.is_ok()); + } + + // Bench time to produce a compressed SNARK + group.bench_function("Prove", |b| { + b.iter(|| { + assert!(CompressedSNARK::<_, _, _, _, SS1, SS2>::prove( + black_box(&pp), + black_box(&pk), + black_box(&recursive_snark) + ) + .is_ok()); + }) + }); + let res = CompressedSNARK::<_, _, _, _, SS1, SS2>::prove(&pp, &pk, &recursive_snark); + assert!(res.is_ok()); + let compressed_snark = res.unwrap(); + + // Benchmark the verification time + group.bench_function("Verify", |b| { + b.iter(|| { + assert!(black_box(&compressed_snark) + .verify( + black_box(&vk), + black_box(num_steps), + black_box(vec![::Scalar::from(2u64)]), + black_box(vec![::Scalar::from(2u64)]), + ) + .is_ok()); + }) + }); + + group.finish(); + } +} + #[derive(Clone, Debug, Default)] struct NonTrivialTestCircuit { num_cons: usize, diff --git a/benches/compute-digest.rs b/benches/compute-digest.rs index 47bdda1dc..501055656 100644 --- a/benches/compute-digest.rs +++ b/benches/compute-digest.rs @@ -27,7 +27,7 @@ criterion_main!(compute_digest); fn bench_compute_digest(c: &mut Criterion) { c.bench_function("compute_digest", |b| { b.iter(|| { - PublicParams::::setup(black_box(C1::new(10)), black_box(C2::default())) + PublicParams::::setup(black_box(&C1::new(10)), black_box(&C2::default())) }) }); } diff --git a/benches/recursive-snark.rs b/benches/recursive-snark.rs index eed8d48fa..5af803f83 100644 --- a/benches/recursive-snark.rs +++ b/benches/recursive-snark.rs @@ -56,7 +56,7 @@ fn bench_recursive_snark(c: &mut Criterion) { let c_secondary = TrivialTestCircuit::default(); // Produce public parameters - let pp = PublicParams::::setup(c_primary.clone(), c_secondary.clone()); + let pp = PublicParams::::setup(&c_primary, &c_secondary); // Bench time to produce a recursive SNARK; // we execute a certain number of warm-up steps since executing diff --git a/benches/sha256.rs b/benches/sha256.rs index f35500f86..642c69912 100644 --- a/benches/sha256.rs +++ b/benches/sha256.rs @@ -200,8 +200,8 @@ fn bench_recursive_snark(c: &mut Criterion) { group.sample_size(10); // Produce public parameters - let pp = - PublicParams::::setup(circuit_primary.clone(), TrivialTestCircuit::default()); + let ttc = TrivialTestCircuit::default(); + let pp = PublicParams::::setup(&circuit_primary, &ttc); let circuit_secondary = TrivialTestCircuit::default(); let z0_primary = vec![::Scalar::from(2u64)]; diff --git a/examples/minroot.rs b/examples/minroot.rs index 75c2d41df..dd5c8d60c 100644 --- a/examples/minroot.rs +++ b/examples/minroot.rs @@ -172,7 +172,7 @@ fn main() { G2, MinRootCircuit<::Scalar>, TrivialTestCircuit<::Scalar>, - >::setup(circuit_primary.clone(), circuit_secondary.clone()); + >::setup(&circuit_primary, &circuit_secondary); println!("PublicParams::setup, took {:?} ", start.elapsed()); println!( diff --git a/src/bellperson/r1cs.rs b/src/bellperson/r1cs.rs index 56710f76e..ae3388dfb 100644 --- a/src/bellperson/r1cs.rs +++ b/src/bellperson/r1cs.rs @@ -28,10 +28,7 @@ pub trait NovaShape { fn r1cs_shape(&self) -> (R1CSShape, CommitmentKey); } -impl NovaWitness for SatisfyingAssignment -where - G::Scalar: PrimeField, -{ +impl NovaWitness for SatisfyingAssignment { fn r1cs_instance_and_witness( &self, shape: &R1CSShape, @@ -48,10 +45,7 @@ where } } -impl NovaShape for ShapeCS -where - G::Scalar: PrimeField, -{ +impl NovaShape for ShapeCS { fn r1cs_shape(&self) -> (R1CSShape, CommitmentKey) { let mut A: Vec<(usize, usize, G::Scalar)> = Vec::new(); let mut B: Vec<(usize, usize, G::Scalar)> = Vec::new(); diff --git a/src/bellperson/shape_cs.rs b/src/bellperson/shape_cs.rs index bb964636d..a80be8c55 100644 --- a/src/bellperson/shape_cs.rs +++ b/src/bellperson/shape_cs.rs @@ -48,10 +48,7 @@ impl Ord for OrderedVariable { #[allow(clippy::upper_case_acronyms)] /// `ShapeCS` is a `ConstraintSystem` for creating `R1CSShape`s for a circuit. -pub struct ShapeCS -where - G::Scalar: PrimeField + Field, -{ +pub struct ShapeCS { named_objects: HashMap, current_namespace: Vec, #[allow(clippy::type_complexity)] @@ -92,10 +89,7 @@ fn proc_lc( map } -impl ShapeCS -where - G::Scalar: PrimeField, -{ +impl ShapeCS { /// Create a new, default `ShapeCS`, pub fn new() -> Self { ShapeCS::default() @@ -216,10 +210,7 @@ where } } -impl Default for ShapeCS -where - G::Scalar: PrimeField, -{ +impl Default for ShapeCS { fn default() -> Self { let mut map = HashMap::new(); map.insert("ONE".into(), NamedObject::Var(ShapeCS::::one())); @@ -233,10 +224,7 @@ where } } -impl ConstraintSystem for ShapeCS -where - G::Scalar: PrimeField, -{ +impl ConstraintSystem for ShapeCS { type Root = Self; fn alloc(&mut self, annotation: A, _f: F) -> Result diff --git a/src/bellperson/solver.rs b/src/bellperson/solver.rs index 0eaf088ce..2357724a9 100644 --- a/src/bellperson/solver.rs +++ b/src/bellperson/solver.rs @@ -1,7 +1,7 @@ //! Support for generating R1CS witness using bellperson. use crate::traits::Group; -use ff::{Field, PrimeField}; +use ff::Field; use bellperson::{ multiexp::DensityTracker, ConstraintSystem, Index, LinearCombination, SynthesisError, Variable, @@ -9,10 +9,7 @@ use bellperson::{ /// A `ConstraintSystem` which calculates witness values for a concrete instance of an R1CS circuit. #[derive(PartialEq)] -pub struct SatisfyingAssignment -where - G::Scalar: PrimeField, -{ +pub struct SatisfyingAssignment { // Density of queries a_aux_density: DensityTracker, b_input_density: DensityTracker, @@ -29,10 +26,7 @@ where } use std::fmt; -impl fmt::Debug for SatisfyingAssignment -where - G::Scalar: PrimeField, -{ +impl fmt::Debug for SatisfyingAssignment { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt .debug_struct("SatisfyingAssignment") @@ -69,10 +63,7 @@ where } } -impl ConstraintSystem for SatisfyingAssignment -where - G::Scalar: PrimeField, -{ +impl ConstraintSystem for SatisfyingAssignment { type Root = Self; fn new() -> Self { diff --git a/src/ccs/cccs.rs b/src/ccs/cccs.rs new file mode 100644 index 000000000..6c67f6214 --- /dev/null +++ b/src/ccs/cccs.rs @@ -0,0 +1,304 @@ +use crate::hypercube::BooleanHypercube; +use crate::spartan::math::Math; +use crate::spartan::polynomial::MultilinearPolynomial; +use crate::{ + constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_FE_FOR_RO, NUM_HASH_BITS}, + errors::NovaError, + gadgets::{ + nonnative::{bignat::nat_to_limbs, util::f_to_nat}, + utils::scalar_as_base, + }, + r1cs::{R1CSInstance, R1CSShape, R1CSWitness, R1CS}, + traits::{ + commitment::CommitmentEngineTrait, commitment::CommitmentTrait, AbsorbInROTrait, Group, ROTrait, + }, + utils::*, + Commitment, CommitmentKey, CE, +}; +use bitvec::vec; +use core::{cmp::max, marker::PhantomData}; +use ff::{Field, PrimeField}; +use itertools::concat; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use std::ops::{Add, Mul}; +use std::sync::Arc; + +use super::util::compute_sum_Mz; +use super::util::virtual_poly::VirtualPolynomial; +use super::CCS; + +/// A type that holds the shape of a Committed CCS (CCCS) instance +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct CCCS { + // The `x` vector represents public IO. + pub(crate) x: Option>, + // Commitment to the witness of `z`. + pub(crate) w_comm: Commitment, +} + +impl CCCS { + /// Generates a new CCCS given a reference to it's original CCS repr and it's public and private inputs. + pub(crate) fn new( + ccs: &CCS, + ccs_matrix_mle: &[MultilinearPolynomial], + z: Vec, + ck: &CommitmentKey, + ) -> Self { + Self { + x: if ccs.l == 0 { + None + } else { + Some(z[(1..ccs.l + 1)].to_vec()) + }, + w_comm: CE::::commit(ck, &z[(1 + ccs.l)..]), + } + } + + pub(crate) fn construct_z(&self, witness: &[G::Scalar]) -> Vec { + concat(vec![ + vec![G::Scalar::ONE], + self.x.clone().unwrap_or(vec![]), + witness.to_vec(), + ]) + } + + /// Computes q(x) = \sum^q c_i * \prod_{j \in S_i} ( \sum_{y \in {0,1}^s'} M_j(x, y) * z(y) ) + /// polynomial over x + pub(crate) fn compute_q( + &self, + ccs: &CCS, + ccs_mles: &[MultilinearPolynomial], + witness: &[G::Scalar], + ) -> Result, NovaError> { + let tmp_z = self.construct_z(witness); + let z_mle = dense_vec_to_mle::(ccs.s_prime, &tmp_z); + if z_mle.get_num_vars() != ccs.s_prime { + // this check if redundant if dense_vec_to_mle is correct + return Err(NovaError::VpArith); + } + + // Using `fold` requires to not have results inside. So we unwrap for now but + // a better approach is needed (we ca just keep the for loop otherwise.) + Ok( + (0..ccs.q).fold(VirtualPolynomial::::new(ccs.s), |q, idx| { + let mut prod = VirtualPolynomial::::new(ccs.s); + + for &j in &ccs.S[idx] { + let sum_Mz = compute_sum_Mz::(&ccs_mles[j], &z_mle); + + // Fold this sum into the running product + if prod.products.is_empty() { + // If this is the first time we are adding something to this virtual polynomial, we need to + // explicitly add the products using add_mle_list() + // XXX is this true? improve API + prod + .add_mle_list([Arc::new(sum_Mz)], G::Scalar::ONE) + .unwrap(); + } else { + prod.mul_by_mle(Arc::new(sum_Mz), G::Scalar::ONE).unwrap(); + } + } + // Multiply by the product by the coefficient c_i + prod.scalar_mul(&ccs.c[idx]); + // Add it to the running sum + q.add(&prod) + }), + ) + } + + /// Computes Q(x) = eq(beta, x) * q(x) + /// = eq(beta, x) * \sum^q c_i * \prod_{j \in S_i} ( \sum_{y \in {0,1}^s'} M_j(x, y) * z(y) ) + /// polynomial over x + pub fn compute_Q( + &self, + ccs: &CCS, + ccs_mles: &[MultilinearPolynomial], + beta: &[G::Scalar], + witness: &[G::Scalar], + ) -> Result, NovaError> { + let q = self.compute_q(ccs, ccs_mles, witness)?; + q.build_f_hat(beta) + } + + /// Perform the check of the CCCS instance described at section 4.1 + pub fn is_sat( + &self, + ccs: &CCS, + ccs_mles: &[MultilinearPolynomial], + ck: &CommitmentKey, + witness: &[G::Scalar], + ) -> Result<(), NovaError> { + // check that C is the commitment of w. Notice that this is not verifying a Pedersen + // opening, but checking that the Commmitment comes from committing to the witness. + assert_eq!(self.w_comm, CE::::commit(ck, witness)); + + // A CCCS relation is satisfied if the q(x) multivariate polynomial evaluates to zero in the hypercube + let q_x = self.compute_q(ccs, ccs_mles, witness).unwrap(); + for x in BooleanHypercube::new(ccs.s) { + if !q_x.evaluate(&x).unwrap().is_zero().unwrap_u8() == 0 { + return Err(NovaError::UnSat); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use crate::ccs::CCSInstance; + use crate::ccs::CCSWitness; + + use super::*; + use ff::PrimeField; + use pasta_curves::pallas::Scalar; + use pasta_curves::Ep; + use pasta_curves::Fp; + use pasta_curves::Fq; + use rand_core::OsRng; + use rand_core::RngCore; + + // Deduplicate this + fn to_F_matrix(m: Vec>) -> Vec> { + m.iter().map(|x| to_F_vec(x.clone())).collect() + } + + // Deduplicate this + fn to_F_vec(v: Vec) -> Vec { + v.iter().map(|x| F::from(*x)).collect() + } + + fn vecs_to_slices(vecs: &[Vec]) -> Vec<&[T]> { + vecs.iter().map(Vec::as_slice).collect() + } + + fn test_compute_q_with() { + let mut rng = OsRng; + + let z = CCS::::get_test_z(3); + let (ccs, ccs_witness, ccs_instance, mles) = CCS::::gen_test_ccs(&z); + + // generate ck + let ck = CCS::::commitment_key(&ccs); + // ensure CCS is satisfied + ccs.is_sat(&ck, &ccs_instance, &ccs_witness).unwrap(); + + // Generate CCCS artifacts + let cccs = CCCS::new(&ccs, &mles, z, &ck); + let q = cccs.compute_q(&ccs, &mles, &ccs_witness.w).unwrap(); + + // Evaluate inside the hypercube + BooleanHypercube::new(ccs.s).for_each(|x| { + assert_eq!(G::Scalar::ZERO, q.evaluate(&x).unwrap()); + }); + + // Evaluate outside the hypercube + let beta: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut rng)).collect(); + assert_ne!(G::Scalar::ZERO, q.evaluate(&beta).unwrap()); + } + + fn test_compute_Q_with() { + let mut rng = OsRng; + + let z = CCS::::get_test_z(3); + let (ccs, ccs_witness, ccs_instance, mles) = CCS::::gen_test_ccs(&z); + + // generate ck + let ck = CCS::::commitment_key(&ccs); + // ensure CCS is satisfied + ccs.is_sat(&ck, &ccs_instance, &ccs_witness).unwrap(); + + // Generate CCCS artifacts + let cccs = CCCS::new(&ccs, &mles, z, &ck); + let beta: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut rng)).collect(); + // Compute Q(x) = eq(beta, x) * q(x). + let Q = cccs + .compute_Q(&ccs, &mles, &beta, &ccs_witness.w) + .expect("Computation of Q should not fail"); + + // Let's consider the multilinear polynomial G(x) = \sum_{y \in {0, 1}^s} eq(x, y) q(y) + // which interpolates the multivariate polynomial q(x) inside the hypercube. + // + // Observe that summing Q(x) inside the hypercube, directly computes G(\beta). + // + // Now, G(x) is multilinear and agrees with q(x) inside the hypercube. Since q(x) vanishes inside the + // hypercube, this means that G(x) also vanishes in the hypercube. Since G(x) is multilinear and vanishes + // inside the hypercube, this makes it the zero polynomial. + // + // Hence, evaluating G(x) at a random beta should give zero. + + // Now sum Q(x) evaluations in the hypercube and expect it to be 0 + let r = BooleanHypercube::new(ccs.s) + .map(|x| Q.evaluate(&x).unwrap()) + .fold(G::Scalar::ZERO, |acc, result| acc + result); + assert_eq!(r, G::Scalar::ZERO); + } + + fn test_Q_against_q_with() { + let mut rng = OsRng; + + let z = CCS::::get_test_z(3); + let (ccs, ccs_witness, ccs_instance, mles) = CCS::::gen_test_ccs(&z); + + // generate ck + let ck = CCS::::commitment_key(&ccs); + // ensure CCS is satisfied + ccs.is_sat(&ck, &ccs_instance, &ccs_witness).unwrap(); + + // Generate CCCS artifacts + let cccs = CCCS::new(&ccs, &mles, z, &ck); + // Now test that if we create Q(x) with eq(d,y) where d is inside the hypercube, \sum Q(x) should be G(d) which + // should be equal to q(d), since G(x) interpolates q(x) inside the hypercube + let q = cccs + .compute_q(&ccs, &mles, &ccs_witness.w) + .expect("Computing q shoud not fail"); + + for d in BooleanHypercube::new(ccs.s) { + let Q_at_d = cccs + .compute_Q(&ccs, &mles, &d, &ccs_witness.w) + .expect("Computing Q_at_d shouldn't fail"); + + // Get G(d) by summing over Q_d(x) over the hypercube + let G_at_d = BooleanHypercube::new(ccs.s) + .map(|x| Q_at_d.evaluate(&x).unwrap()) + .fold(G::Scalar::ZERO, |acc, result| acc + result); + assert_eq!(G_at_d, q.evaluate(&d).unwrap()); + } + + // Now test that they should disagree outside of the hypercube + let r: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut rng)).collect(); + let Q_at_r = cccs + .compute_Q(&ccs, &mles, &r, &ccs_witness.w) + .expect("Computing Q_at_r shouldn't fail"); + + // Get G(d) by summing over Q_d(x) over the hypercube + let G_at_r = BooleanHypercube::new(ccs.s) + .map(|x| Q_at_r.evaluate(&x).unwrap()) + .fold(G::Scalar::ZERO, |acc, result| acc + result); + assert_ne!(G_at_r, q.evaluate(&r).unwrap()); + } + + /// Do some sanity checks on q(x). It's a multivariable polynomial and it should evaluate to zero inside the + /// hypercube, but to not-zero outside the hypercube. + #[test] + fn test_compute_q() { + test_compute_q_with::(); + } + + #[test] + fn test_compute_Q() { + test_compute_Q_with::(); + } + + /// The polynomial G(x) (see above) interpolates q(x) inside the hypercube. + /// Summing Q(x) over the hypercube is equivalent to evaluating G(x) at some point. + /// This test makes sure that G(x) agrees with q(x) inside the hypercube, but not outside + #[test] + fn test_Q_against_q() { + test_Q_against_q_with::(); + } +} diff --git a/src/ccs/lcccs.rs b/src/ccs/lcccs.rs new file mode 100644 index 000000000..96c6f01a3 --- /dev/null +++ b/src/ccs/lcccs.rs @@ -0,0 +1,245 @@ +use super::util::{compute_sum_Mz, VirtualPolynomial}; +use super::{CCSWitness, CCCS, CCS}; +use crate::ccs::util::compute_all_sum_Mz_evals; +use crate::hypercube::BooleanHypercube; +use crate::spartan::math::Math; +use crate::spartan::polynomial::MultilinearPolynomial; +use crate::{ + constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_FE_FOR_RO, NUM_HASH_BITS}, + errors::NovaError, + gadgets::{ + nonnative::{bignat::nat_to_limbs, util::f_to_nat}, + utils::scalar_as_base, + }, + r1cs::{R1CSInstance, R1CSShape, R1CSWitness, R1CS}, + traits::{ + commitment::CommitmentEngineTrait, commitment::CommitmentTrait, AbsorbInROTrait, Group, ROTrait, + }, + utils::*, + Commitment, CommitmentKey, CE, +}; +use bitvec::vec; +use core::{cmp::max, marker::PhantomData}; +use ff::{Field, PrimeField}; +use itertools::concat; +use rand_core::RngCore; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use std::ops::{Add, Mul}; +use std::sync::Arc; + +/// A type that holds a LCCCS instance +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct LCCCS { + /// Commitment to witness + pub(crate) w_comm: Commitment, + /// Vector of v_i (result of folding thetas and sigmas). + pub(crate) v: Vec, + /// Random evaluation point for the v_i + pub(crate) r_x: Vec, + /// Public input/output + pub(crate) x: Option>, + /// Relaxation factor of z for folded LCCCS + pub(crate) u: G::Scalar, +} + +impl LCCCS { + /// Generates a new LCCCS instance from a given randomness, CommitmentKey & witness input vector. + /// This should only be used to probably test or setup the initial NIMFS instance. + pub(crate) fn new( + ccs: &CCS, + ccs_m_mle: &[MultilinearPolynomial], + ck: &CommitmentKey, + z: Vec, + r_x: Vec, + ) -> Self { + let w_comm = <::CE as CommitmentEngineTrait>::commit(ck, &z[(1 + ccs.l)..]); + + // Evaluation points for `v` + let v = ccs.compute_v_j(&z, &r_x, ccs_m_mle); + + // Circuit might not have public IO. Hence, if so, we default it to zero. + let x = if ccs.l == 0 { + None + } else { + Some(z[1..ccs.l + 1].to_vec()) + }; + + Self { + w_comm, + v, + r_x, + u: G::Scalar::ONE, + x, + } + } + + pub(crate) fn construct_z(&self, witness: &[G::Scalar]) -> Vec { + concat(vec![ + vec![self.u], + self.x.clone().unwrap_or(vec![]), + witness.to_vec(), + ]) + } + + /// Checks if the CCS instance is satisfiable given a witness and its shape + pub fn is_sat( + &self, + ccs: &CCS, + ccs_m_mle: &[MultilinearPolynomial], + ck: &CommitmentKey, + witness: &[G::Scalar], + ) -> Result<(), NovaError> { + // check that C is the commitment of w. Notice that this is not verifying a Pedersen + // opening, but checking that the Commmitment comes from committing to the witness. + let comm_eq = self.w_comm == CE::::commit(ck, witness); + + let computed_v = compute_all_sum_Mz_evals::( + ccs_m_mle, + &self.construct_z(witness), + &self.r_x, + ccs.s_prime, + ); + + let vs_eq = computed_v == self.v; + + if vs_eq && comm_eq { + Ok(()) + } else { + Err(NovaError::UnSat) + } + } + + /// Compute all L_j(x) polynomials. + pub fn compute_Ls( + &self, + ccs: &CCS, + ccs_m_mle: &[MultilinearPolynomial], + lcccs_witness: &[G::Scalar], + ) -> Vec> { + let z_mle = dense_vec_to_mle(ccs.s_prime, self.construct_z(lcccs_witness).as_slice()); + + let mut vec_L_j_x = Vec::with_capacity(ccs.t); + for M_j in ccs_m_mle.iter() { + // Sanity check + assert_eq!(z_mle.get_num_vars(), ccs.s_prime); + + let sum_Mz = compute_sum_Mz::(M_j, &z_mle); + let sum_Mz_virtual = VirtualPolynomial::new_from_mle(&Arc::new(sum_Mz), G::Scalar::ONE); + let L_j_x = sum_Mz_virtual.build_f_hat(&self.r_x).unwrap(); + vec_L_j_x.push(L_j_x); + } + + vec_L_j_x + } +} + +#[cfg(test)] +mod tests { + use pasta_curves::{Ep, Fq}; + use rand_core::OsRng; + + use super::*; + + fn satisfied_ccs_is_satisfied_lcccs_with() { + // Gen test vectors & artifacts + let z = CCS::::get_test_z(3); + let (ccs, witness, instance, mles) = CCS::::gen_test_ccs(&z); + let ck = ccs.commitment_key(); + assert!(ccs.is_sat(&ck, &instance, &witness).is_ok()); + + // LCCCS with the correct z should pass + let r_x: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut OsRng)).collect(); + let mut lcccs = LCCCS::new(&ccs, &mles, &ck, z.clone(), r_x); + assert!(lcccs.is_sat(&ccs, &mles, &ck, &witness.w).is_ok()); + + // Wrong witness so that the relation does not hold + let mut bad_witness = witness.w.clone(); + bad_witness[2] = G::Scalar::ZERO; + + // LCCCS with the wrong z should not pass `is_sat`. + assert!(lcccs.is_sat(&ccs, &mles, &ck, &bad_witness).is_err()); + } + + fn test_lcccs_v_j_with() { + let mut rng = OsRng; + + // Gen test vectors & artifacts + let z = CCS::::get_test_z(3); + let (ccs, witness, _, mles) = CCS::::gen_test_ccs(&z); + let ck = ccs.commitment_key(); + + let r_x: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut rng)).collect(); + + // Get LCCCS + let lcccs = LCCCS::new(&ccs, &mles, &ck, z, r_x); + + let vec_L_j_x = lcccs.compute_Ls(&ccs, &mles, &witness.w); + assert_eq!(vec_L_j_x.len(), lcccs.v.len()); + + for (v_i, L_j_x) in lcccs.v.into_iter().zip(vec_L_j_x) { + let sum_L_j_x = BooleanHypercube::new(ccs.s) + .map(|y| L_j_x.evaluate(&y).unwrap()) + .fold(G::Scalar::ZERO, |acc, result| acc + result); + assert_eq!(v_i, sum_L_j_x); + } + } + + fn test_bad_v_j_with() { + let mut rng = OsRng; + + // Gen test vectors & artifacts + let z = CCS::::get_test_z(3); + let (ccs, witness, instance, mles) = CCS::::gen_test_ccs(&z); + let ck = ccs.commitment_key(); + + // Mutate witness so that the relation does not hold + let mut bad_witness = witness.w.clone(); + bad_witness[2] = G::Scalar::ZERO; + + // Compute v_j with the right z + let r_x: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut rng)).collect(); + let mut lcccs = LCCCS::new(&ccs, &mles, &ck, z, r_x); + // Assert LCCCS is satisfied with the original Z + assert!(lcccs.is_sat(&ccs, &mles, &ck, &witness.w).is_ok()); + + // Compute L_j(x) with the bad z + let vec_L_j_x = lcccs.compute_Ls(&ccs, &mles, &bad_witness); + assert_eq!(vec_L_j_x.len(), lcccs.v.len()); + // Assert LCCCS is not satisfied with the bad Z + assert!(lcccs.is_sat(&ccs, &mles, &ck, &bad_witness).is_err()); + + // Make sure that the LCCCS is not satisfied given these L_j(x) + // i.e. summing L_j(x) over the hypercube should not give v_j for all j + let mut satisfied = true; + for (v_i, L_j_x) in lcccs.v.into_iter().zip(vec_L_j_x) { + let sum_L_j_x = BooleanHypercube::new(ccs.s) + .map(|y| L_j_x.evaluate(&y).unwrap()) + .fold(G::Scalar::ZERO, |acc, result| acc + result); + if v_i != sum_L_j_x { + satisfied = false; + } + } + + assert!(!satisfied); + } + + #[test] + fn satisfied_ccs_is_satisfied_lcccs() { + satisfied_ccs_is_satisfied_lcccs_with::(); + } + + #[test] + /// Test linearized CCCS v_j against the L_j(x) + fn test_lcccs_v_j() { + test_lcccs_v_j_with::(); + } + + /// Given a bad z, check that the v_j should not match with the L_j(x) + #[test] + fn test_bad_v_j() { + test_bad_v_j_with::(); + } +} diff --git a/src/ccs/mod.rs b/src/ccs/mod.rs new file mode 100644 index 000000000..d56a6af77 --- /dev/null +++ b/src/ccs/mod.rs @@ -0,0 +1,458 @@ +//! This module defines CCS related types and functions. +#![allow(unused_imports)] +#![allow(dead_code)] +#![allow(unused)] +#![allow(clippy::type_complexity)] + +use crate::hypercube::BooleanHypercube; +use crate::spartan::math::Math; +use crate::spartan::polynomial::MultilinearPolynomial; +use crate::{ + constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_FE_FOR_RO, NUM_HASH_BITS}, + errors::NovaError, + gadgets::{ + nonnative::{bignat::nat_to_limbs, util::f_to_nat}, + utils::scalar_as_base, + }, + r1cs::{R1CSInstance, R1CSShape, R1CSWitness, R1CS}, + traits::{ + commitment::CommitmentEngineTrait, commitment::CommitmentTrait, AbsorbInROTrait, Group, ROTrait, + }, + utils::*, + Commitment, CommitmentKey, CE, +}; +use bitvec::vec; +use core::{cmp::max, marker::PhantomData}; +use ff::Field; +use itertools::concat; +use rand_core::RngCore; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use std::ops::{Add, Mul}; + +pub use cccs::CCCS; +pub use lcccs::LCCCS; +pub use multifolding::NIMFS; +use util::compute_all_sum_Mz_evals; + +mod cccs; +mod lcccs; +mod multifolding; +mod util; + +/// A type that holds the shape of a CCS instance +/// Unlike R1CS we have a list of matrices M instead of only A, B, C +/// We also have t, q, d constants and c (vector), S (set) +/// As well as m, n, s, s_prime +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct CCS { + pub(crate) M: Vec>, + // Num vars + pub(crate) t: usize, + // Number of public witness + pub(crate) l: usize, + pub(crate) q: usize, + pub(crate) d: usize, + pub(crate) S: Vec>, + + // Was: usize + pub(crate) c: Vec, + + // n is the number of columns in M_i + pub(crate) n: usize, + // m is the number of rows in M_i + pub(crate) m: usize, + // s = log m + pub(crate) s: usize, + // s_prime = log n + pub(crate) s_prime: usize, +} + +impl CCS { + /// Create an object of type `CCS` from the explicitly specified CCS matrices + pub fn new( + M: &[SparseMatrix], + t: usize, + l: usize, + q: usize, + d: usize, + S: Vec>, + c: Vec, + ) -> CCS { + // Can probably be made more efficient by keeping track fo n_rows/n_cols at creation/insert time + let m = M + .iter() + .fold(usize::MIN, |acc, matrix| max(acc, matrix.n_rows())); + let n = M + .iter() + .fold(usize::MIN, |acc, matrix| max(acc, matrix.n_cols())); + + // Check that the row and column indexes are within the range of the number of constraints and variables + assert!(M + .iter() + .map(|matrix| matrix.is_valid(m, t, l)) + .collect::, NovaError>>() + .is_ok()); + + // We require the number of public inputs/outputs to be even + assert_ne!(l % 2, 0, " number of public i/o has to be even"); + + let s = m.log_2(); + let s_prime = n.log_2(); + + CCS { + M: M.to_vec(), + t, + l, + q, + d, + S, + c, + m, + n, + s, + s_prime, + } + } + + /// Compute v_j values of the linearized committed CCS form + /// Given `r`, compute: \sum_{y \in {0,1}^s'} M_j(r, y) * z(y) + fn compute_v_j( + &self, + z: &[G::Scalar], + r: &[G::Scalar], + ccs_matrix_mles: &[MultilinearPolynomial], + ) -> Vec { + compute_all_sum_Mz_evals::(ccs_matrix_mles, z, r, self.s_prime) + } + + // XXX: Update commitment_key variables here? This is currently based on R1CS with M length + /// Samples public parameters for the specified number of constraints and variables in an CCS + pub fn commitment_key(&self) -> CommitmentKey { + let total_nz = self.M.iter().fold(0, |acc, m| acc + m.coeffs().len()); + + G::CE::setup(b"ck", max(max(self.m, self.t), total_nz)) + } + + /// Checks if the CCS instance is satisfiable given a witness and its shape + // XXX: Probably is better to completelly remove the abstraction of Instance and witness and just deal with Z. + pub fn is_sat( + &self, + ck: &CommitmentKey, + U: &CCSInstance, + W: &CCSWitness, + ) -> Result<(), NovaError> { + assert_eq!(W.w.len(), self.n - self.l - 1); + assert_eq!(U.x.len(), self.l); + + // Sage code to check CCS relation: + // + // r = [F(0)] * m + // for i in range(0, q): + // hadamard_output = [F(1)]*m + // for j in S[i]: + // hadamard_output = hadamard_product(hadamard_output, + // matrix_vector_product(M[j], z)) + // + // r = vec_add(r, vec_elem_mul(hadamard_output, c[i])) + // print("\nCCS relation check (∑ cᵢ ⋅ ◯ Mⱼ z == 0):", r == [0]*m) + // + // verify if ∑ cᵢ ⋅ ◯ Mⱼ z == 0 + + let z = concat(vec![vec![G::Scalar::ONE], U.x.clone(), W.w.clone()]); + + let r = (0..self.q).fold(vec![G::Scalar::ZERO; self.m], |r, idx| { + let hadamard_output = self.S[idx] + .iter() + .fold(vec![G::Scalar::ZERO; self.m], |acc, j| { + let mvp = matrix_vector_product_sparse(&self.M[*j], &z); + hadamard_product(&acc, &mvp) + }); + + // Multiply by the coefficient of this step + let c_M_j_z: Vec<::Scalar> = vector_elem_product(&hadamard_output, self.c[idx]); + + vector_add(&r, &c_M_j_z) + }); + + // verify if comm_W is a commitment to W + let res_comm: bool = U.comm_w == CE::::commit(ck, &W.w); + + if r == vec![G::Scalar::ZERO; self.m] && res_comm { + Ok(()) + } else { + Err(NovaError::UnSat) + } + } + + /// Generate a CCS instance from an [`R1CSShape`] instance. + pub fn from_r1cs(r1cs: R1CSShape) -> Self { + // These contants are used for R1CS-to-CCS, see the paper for more details + const T: usize = 3; + const Q: usize = 2; + const D: usize = 2; + const S1: [usize; 2] = [0, 1]; + const S2: [usize; 1] = [2]; + + // Generate the SparseMatrix vec + let A = SparseMatrix::with_coeffs(r1cs.num_cons, r1cs.num_vars, r1cs.A); + let B = SparseMatrix::with_coeffs(r1cs.num_cons, r1cs.num_vars, r1cs.B); + let C = SparseMatrix::with_coeffs(r1cs.num_cons, r1cs.num_vars, r1cs.C); + + // Assert all matrixes have the same row/column length. + assert_eq!(A.n_cols(), B.n_cols()); + assert_eq!(B.n_cols(), C.n_cols()); + assert_eq!(A.n_rows(), B.n_rows()); + assert_eq!(B.n_rows(), C.n_rows()); + + Self { + M: vec![A, B, C], + t: T, + l: r1cs.num_io, + q: Q, + d: D, + S: vec![S1.to_vec(), S2.to_vec()], + c: vec![G::Scalar::ONE, -G::Scalar::ONE], + m: r1cs.num_cons, + n: r1cs.num_vars, + s: r1cs.num_cons.log_2(), + s_prime: r1cs.num_vars.log_2(), + } + } + + /// Pads the CCS so that the number of variables is a power of two + /// Renumbers variables to accomodate padded variables + pub fn pad(&mut self) { + let padded_n = self.n.next_power_of_two(); + + // check if the number of variables are as expected, then + // we simply set the number of constraints to the next power of two + if self.n != padded_n { + // Apply pad for each matrix in M + self.M.iter_mut().for_each(|m| { + m.pad(); + *m.n_rows_mut() = padded_n + }); + self.n = padded_n; + } + } + + #[cfg(test)] + pub(crate) fn gen_test_ccs( + z: &[G::Scalar], + ) -> ( + CCS, + CCSWitness, + CCSInstance, + Vec>, + ) { + let one = G::Scalar::ONE; + let A = vec![ + (0, 1, one), + (1, 3, one), + (2, 1, one), + (2, 4, one), + (3, 0, G::Scalar::from(5u64)), + (3, 5, one), + ]; + + let B = vec![(0, 1, one), (1, 1, one), (2, 0, one), (3, 0, one)]; + let C = vec![(0, 3, one), (1, 4, one), (2, 5, one), (3, 2, one)]; + + // 2. Take R1CS and convert to CCS + // TODO: The third argument should be 2 or similar, need to adjust test case + // See https://github.com/privacy-scaling-explorations/Nova/issues/30 + let ccs = CCS::from_r1cs(R1CSShape::new(4, 6, 1, &A, &B, &C).unwrap()); + // Generate other artifacts + let ck = CCS::::commitment_key(&ccs); + let ccs_w = CCSWitness::new(z[2..].to_vec()); + let ccs_instance = CCSInstance::new(&ccs, &ccs_w.commit(&ck), vec![z[1]]).unwrap(); + let ccs_mles = ccs.M.iter().map(|m| m.to_mle()).collect(); + + ccs + .is_sat(&ck, &ccs_instance, &ccs_w) + .expect("This does not fail"); + (ccs, ccs_w, ccs_instance, ccs_mles) + } + + #[cfg(test)] + /// Computes the z vector for the given input for Vitalik's equation. + pub(crate) fn get_test_z(input: u64) -> Vec { + // z = (1, io, w) + let input = G::Scalar::from(input); + vec![ + G::Scalar::ONE, + input, + input * input * input + input + G::Scalar::from(5u64), // x^3 + x + 5 + input * input, // x^2 + input * input * input, // x^2 * x + input * input * input + input, // x^3 + x + ] + } +} + +/// A type that holds a witness for a given CCS instance +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CCSWitness { + // Vector W in F^{n - l - 1} + w: Vec, +} + +impl CCSWitness { + /// Create a CCSWitness instance from the witness vector. + pub fn new(witness: Vec) -> Self { + Self { w: witness } + } + + /// Commits to the witness using the supplied generators + pub fn commit(&self, ck: &CommitmentKey) -> Commitment { + CE::::commit(ck, &self.w) + } +} +/// A type that holds an CCS instance +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct CCSInstance { + // (Pedersen) Commitment to a witness + pub(crate) comm_w: Commitment, + + // Public input x in F^l + pub(crate) x: Vec, +} + +impl CCSInstance { + /// A method to create an instance object using consitituent elements + pub fn new( + s: &CCS, + w_comm: &Commitment, + x: Vec, + ) -> Result, NovaError> { + assert_eq!(s.l, x.len()); + + Ok(CCSInstance { comm_w: *w_comm, x }) + } +} + +#[cfg(test)] +pub mod test { + use super::*; + use crate::{ + r1cs::R1CS, + traits::{Group, ROConstantsTrait}, + }; + use ::bellperson::{gadgets::num::AllocatedNum, ConstraintSystem, SynthesisError}; + use ff::{Field, PrimeField}; + use rand::rngs::OsRng; + + use pasta_curves::Ep; + + fn test_tiny_ccs_with() { + // 1. Generate valid R1CS Shape + // 2. Convert to CCS + // 3. Test that it is satisfiable + + let one = G::Scalar::ONE; + let (num_cons, num_vars, num_io, A, B, C) = { + let num_cons = 4; + let num_vars = 4; + let num_io = 2; + + // Consider a cubic equation: `x^3 + x + 5 = y`, where `x` and `y` are respectively the input and output. + // The R1CS for this problem consists of the following constraints: + // `I0 * I0 - Z0 = 0` + // `Z0 * I0 - Z1 = 0` + // `(Z1 + I0) * 1 - Z2 = 0` + // `(Z2 + 5) * 1 - I1 = 0` + + // Relaxed R1CS is a set of three sparse matrices (A B C), where there is a row for every + // constraint and a column for every entry in z = (vars, u, inputs) + // An R1CS instance is satisfiable iff: + // Az \circ Bz = u \cdot Cz + E, where z = (vars, 1, inputs) + let mut A: Vec<(usize, usize, G::Scalar)> = Vec::new(); + let mut B: Vec<(usize, usize, G::Scalar)> = Vec::new(); + let mut C: Vec<(usize, usize, G::Scalar)> = Vec::new(); + + // constraint 0 entries in (A,B,C) + // `I0 * I0 - Z0 = 0` + A.push((0, num_vars + 1, one)); + B.push((0, num_vars + 1, one)); + C.push((0, 0, one)); + + // constraint 1 entries in (A,B,C) + // `Z0 * I0 - Z1 = 0` + A.push((1, 0, one)); + B.push((1, num_vars + 1, one)); + C.push((1, 1, one)); + + // constraint 2 entries in (A,B,C) + // `(Z1 + I0) * 1 - Z2 = 0` + A.push((2, 1, one)); + A.push((2, num_vars + 1, one)); + B.push((2, num_vars, one)); + C.push((2, 2, one)); + + // constraint 3 entries in (A,B,C) + // `(Z2 + 5) * 1 - I1 = 0` + A.push((3, 2, one)); + A.push((3, num_vars, one + one + one + one + one)); + B.push((3, num_vars, one)); + C.push((3, num_vars + 2, one)); + + (num_cons, num_vars, num_io, A, B, C) + }; + + // create a R1CS shape object + let S = { + let res = R1CSShape::new(num_cons, num_vars, num_io, &A, &B, &C); + assert!(res.is_ok()); + res.unwrap() + }; + + // 2. Take R1CS and convert to CCS + let S = CCS::from_r1cs(S); + + // generate generators and ro constants + let _ck = S.commitment_key(); + let _ro_consts = >::Constants::new(); + + // 3. Test that CCS is satisfiable + let _rand_inst_witness_generator = + |ck: &CommitmentKey, I: &G::Scalar| -> (G::Scalar, CCSInstance, CCSWitness) { + let i0 = *I; + + // compute a satisfying (vars, X) tuple + let (O, vars, X) = { + let z0 = i0 * i0; // constraint 0 + let z1 = i0 * z0; // constraint 1 + let z2 = z1 + i0; // constraint 2 + let i1 = z2 + one + one + one + one + one; // constraint 3 + + // store the witness and IO for the instance + let W = vec![z0, z1, z2, G::Scalar::ZERO]; + let X = vec![i0, i1]; + (i1, W, X) + }; + + let ccs_w = CCSWitness::new(vars); + + let U = { + let comm_W = ccs_w.commit(ck); + let res = CCSInstance::new(&S, &comm_W, X); + assert!(res.is_ok()); + res.unwrap() + }; + + // check that generated instance is satisfiable + assert!(S.is_sat(ck, &U, &ccs_w).is_ok()); + + (O, U, ccs_w) + }; + } + + #[test] + fn test_tiny_ccs() { + test_tiny_ccs_with::(); + } +} diff --git a/src/ccs/multifolding.rs b/src/ccs/multifolding.rs new file mode 100644 index 000000000..188c5e07b --- /dev/null +++ b/src/ccs/multifolding.rs @@ -0,0 +1,459 @@ +use super::cccs::{self, CCCS}; +use super::lcccs::LCCCS; +use super::util::{compute_sum_Mz, VirtualPolynomial}; +use super::{CCSWitness, CCS}; +use crate::ccs::util::compute_all_sum_Mz_evals; +use crate::hypercube::BooleanHypercube; +use crate::spartan::math::Math; +use crate::spartan::polynomial::{EqPolynomial, MultilinearPolynomial}; +use crate::traits::{TranscriptEngineTrait, TranscriptReprTrait}; +use crate::{ + constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_FE_FOR_RO, NUM_HASH_BITS}, + errors::NovaError, + gadgets::{ + nonnative::{bignat::nat_to_limbs, util::f_to_nat}, + utils::scalar_as_base, + }, + r1cs::{R1CSInstance, R1CSShape, R1CSWitness, R1CS}, + traits::{ + commitment::CommitmentEngineTrait, commitment::CommitmentTrait, AbsorbInROTrait, Group, ROTrait, + }, + utils::*, + Commitment, CommitmentKey, CE, +}; +use bitvec::vec; +use core::{cmp::max, marker::PhantomData}; +use ff::{Field, PrimeField}; +use itertools::concat; +use rand_core::RngCore; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use std::ops::{Add, Mul}; +use std::sync::Arc; + +/// The NIMFS (Non-Interactive MultiFolding Scheme) structure is the center of operations of the folding scheme. +/// Once generated, it allows us to fold any upcomming CCCS instances within it without needing to do much. +// XXX: Pending to add doc examples. +#[derive(Debug)] +pub struct NIMFS { + ccs: CCS, + ccs_mle: Vec>, + ck: CommitmentKey, + lcccs: LCCCS, + transcript: G::TE, +} + +impl NIMFS { + /// Generates a new NIMFS instance based on the given CCS instance, it's matrix mle's, an existing LCCCS instance and a commitment key to the CCS. + pub fn new( + ccs: CCS, + ccs_mle: Vec>, + lcccs: LCCCS, + ck: CommitmentKey, + ) -> Self { + Self { + ccs, + ccs_mle, + ck, + lcccs, + transcript: TranscriptEngineTrait::new(b"NIMFS"), + } + } + + /// Initializes a NIMFS instance given the CCS of it and a first witness vector that satifies it. + // XXX: This should probably return an error as we should check whether is satisfied or not. + pub fn init(ccs: CCS, z: Vec, label: &'static [u8]) -> Self { + let mut transcript: G::TE = TranscriptEngineTrait::new(label); + let ccs_mle: Vec> = + ccs.M.iter().map(|matrix| matrix.to_mle()).collect(); + + // Add the first round of witness to the transcript. + let w: Vec = z[(1 + ccs.l)..].to_vec(); + TranscriptEngineTrait::::absorb(&mut transcript, b"og_w", &w); + + let ck = ccs.commitment_key(); + let w_comm = ::CE::commit(&ck, &w); + + // Query challenge to get initial `r_x`. + let r_x: Vec = vec![ + TranscriptEngineTrait::::squeeze(&mut transcript, b"r_x") + .expect("This should never fail"); + ccs.s + ]; + + // Gen LCCCS initial instance. + let lcccs: LCCCS = LCCCS::new(&ccs, &ccs_mle, &ck, z, r_x); + + Self { + ccs, + ccs_mle, + lcccs, + ck, + transcript, + } + } + + /// Generates a new [`CCCS`] instance ready to be folded. + pub fn new_cccs(&self, z: Vec) -> CCCS { + CCCS::new(&self.ccs, &self.ccs_mle, z, &self.ck) + } + + /// Generates a new `r_x` vector using the NIMFS challenge query method. + pub(crate) fn gen_r_x(&mut self) -> Vec { + vec![ + TranscriptEngineTrait::::squeeze(&mut self.transcript, b"r_x") + .expect("This should never fail"); + self.ccs.s + ] + } + + /// This function checks whether the current IVC after the last fold performed is satisfied and returns an error if it isn't. + pub fn is_sat(&self, witness: &[G::Scalar]) -> Result<(), NovaError> { + self + .lcccs + .is_sat(&self.ccs, &self.ccs_mle, &self.ck, witness) + } + + /// Compute sigma_i and theta_i from step 4. + pub fn compute_sigmas_and_thetas( + &self, + cccs_witness: &[G::Scalar], + cccs: &CCCS, + lcccs_witness: &[G::Scalar], + r_x_prime: &[G::Scalar], + ) -> (Vec, Vec) { + ( + // sigmas + compute_all_sum_Mz_evals::( + &self.ccs_mle, + self.lcccs.construct_z(lcccs_witness).as_slice(), + r_x_prime, + self.ccs.s_prime, + ), + // thetas + compute_all_sum_Mz_evals::( + &self.ccs_mle, + cccs.construct_z(cccs_witness).as_slice(), + r_x_prime, + self.ccs.s_prime, + ), + ) + } + + /// Compute the right-hand-side of step 5 of the NIMFS scheme + pub fn compute_c_from_sigmas_and_thetas( + &self, + sigmas: &[G::Scalar], + thetas: &[G::Scalar], + gamma: G::Scalar, + beta: &[G::Scalar], + r_x_prime: &[G::Scalar], + ) -> G::Scalar { + let mut c = G::Scalar::ZERO; + + let e1 = EqPolynomial::new(self.lcccs.r_x.to_vec()).evaluate(r_x_prime); + let e2 = EqPolynomial::new(beta.to_vec()).evaluate(r_x_prime); + + // (sum gamma^j * e1 * sigma_j) + for (j, sigma_j) in sigmas.iter().enumerate() { + let gamma_j = gamma.pow([j as u64]); + c += gamma_j * e1 * sigma_j; + } + + // + gamma^{t+1} * e2 * sum c_i * prod theta_j + let mut lhs = G::Scalar::ZERO; + for i in 0..self.ccs.q { + let mut prod = G::Scalar::ONE; + for j in self.ccs.S[i].clone() { + prod *= thetas[j]; + } + lhs += self.ccs.c[i] * prod; + } + let gamma_t1 = gamma.pow([(self.ccs.t + 1) as u64]); + c += gamma_t1 * e2 * lhs; + c + } + + /// Compute g(x) polynomial for the given inputs. + pub(crate) fn compute_g( + &self, + lcccs_witness: &[G::Scalar], + cccs: &CCCS, + cccs_witness: &[G::Scalar], + gamma: G::Scalar, + beta: &[G::Scalar], + ) -> VirtualPolynomial { + let mut vec_L = self + .lcccs + .compute_Ls(&self.ccs, &self.ccs_mle, lcccs_witness); + + let mut Q = cccs + .compute_Q(&self.ccs, &self.ccs_mle, beta, cccs_witness) + .expect("Q comp should not fail"); + + let mut g = vec_L[0].clone(); + + for (j, L_j) in vec_L.iter_mut().enumerate().skip(1) { + let gamma_j = gamma.pow([j as u64]); + L_j.scalar_mul(&gamma_j); + g = g.add(L_j); + } + + let gamma_t1 = gamma.pow([(self.ccs.t + 1) as u64]); + Q.scalar_mul(&gamma_t1); + g = g.add(&Q); + g + } + + /// Generates the required elements to be able to fold. + pub fn prepare_folding(&mut self) -> (Vec, G::Scalar) { + // Compute r_x_prime and rho from challenging the transcript. + let r_x_prime = self.gen_r_x(); + // Challenge the transcript once more to obtain `rho` + let rho = TranscriptEngineTrait::::squeeze(&mut self.transcript, b"rho") + .expect("This should not fail"); + (r_x_prime, rho) + } + + /// This folds an upcoming CCCS instance into the running LCCCS instance contained within the NIMFS object. + pub fn fold( + &mut self, + cccs: &CCCS, + sigmas: Vec, + thetas: Vec, + r_x_prime: Vec, + rho: G::Scalar, + ) { + // Compute new v from sigmas and thetas. + let folded_v: Vec = sigmas + .iter() + .zip( + thetas + .iter() + .map(|x_i| *x_i * rho) + .collect::>(), + ) + .map(|(a_i, b_i)| *a_i + b_i) + .collect(); + + // Fold x's + + if self.lcccs.x.is_some() && cccs.x.is_some() { + // Use unsafe and unwrap_unchecked?? + self + .lcccs + .x + .as_mut() + .unwrap() + .iter_mut() + .zip(cccs.x.as_ref().unwrap().iter().map(|x| *x * rho)) + .for_each(|(x_lcccs, x_cccs)| *x_lcccs += x_cccs); + }; + + // Here we perform steps 7 & 8 of the section 5 of the paper. Were we actually fold LCCCS & CCCS instances. + self.lcccs.w_comm += cccs.w_comm.mul(rho); + self.lcccs.v = folded_v; + self.lcccs.r_x = r_x_prime; + self.lcccs.u += rho; + } + + /// Folds the current `z` vector of the upcomming CCCS instance together with the LCCCS instance that is contained inside of the NIMFS object. + pub fn fold_witness( + &mut self, + cccs: &CCCS, + cccs_witness: &[G::Scalar], + lcccs_witness: &mut [G::Scalar], + rho: G::Scalar, + ) { + lcccs_witness + .iter_mut() + .zip(cccs_witness.iter().map(|cccs_w| *cccs_w * rho)) + .for_each(|(a_i, b_i)| *a_i += b_i); + + // XXX: There's no handling of r_w atm. So we will ingore until all folding is implemented, + // let r_w = w1.r_w + rho * w2.r_w; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ccs::test; + use pasta_curves::{Ep, Fq}; + use rand_core::OsRng; + + fn test_compute_g_with() { + let z1 = CCS::::get_test_z(3); + let z2 = CCS::::get_test_z(4); + + let (_, ccs_witness_1, ccs_instance_1, mles) = CCS::::gen_test_ccs(&z1); + let (ccs, ccs_witness_2, ccs_instance_2, _) = CCS::::gen_test_ccs(&z2); + + let ck = ccs.commitment_key(); + + assert!(ccs.is_sat(&ck, &ccs_instance_1, &ccs_witness_1).is_ok()); + assert!(ccs.is_sat(&ck, &ccs_instance_2, &ccs_witness_2).is_ok()); + + let mut rng = OsRng; + let gamma: G::Scalar = G::Scalar::random(&mut OsRng); + let beta: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut OsRng)).collect(); + let r_x: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut OsRng)).collect(); + + let lcccs = LCCCS::new(&ccs, &mles, &ck, z1, r_x); + assert!(lcccs.is_sat(&ccs, &mles, &ck, &ccs_witness_1.w).is_ok()); + let cccs = CCCS::new(&ccs, &mles, z2, &ck); + assert!(cccs.is_sat(&ccs, &mles, &ck, &ccs_witness_2.w).is_ok()); + + let mut sum_v_j_gamma = G::Scalar::ZERO; + for j in 0..lcccs.v.len() { + let gamma_j = gamma.pow([j as u64]); + sum_v_j_gamma += lcccs.v[j] * gamma_j; + } + + let nimfs = NIMFS::::new(ccs.clone(), mles.clone(), lcccs.clone(), ck.clone()); + + // Compute g(x) with that r_x + let g = nimfs.compute_g(&ccs_witness_1.w, &cccs, &ccs_witness_2.w, gamma, &beta); + + // evaluate g(x) over x \in {0,1}^s + let mut g_on_bhc = G::Scalar::ZERO; + for x in BooleanHypercube::new(ccs.s) { + g_on_bhc += g.evaluate(&x).unwrap(); + } + + // evaluate sum_{j \in [t]} (gamma^j * Lj(x)) over x \in {0,1}^s + let mut sum_Lj_on_bhc = G::Scalar::ZERO; + let vec_L = lcccs.compute_Ls(&ccs, &mles, &ccs_witness_1.w); + for x in BooleanHypercube::new(ccs.s) { + for (j, coeff) in vec_L.iter().enumerate() { + let gamma_j = gamma.pow([j as u64]); + sum_Lj_on_bhc += coeff.evaluate(&x).unwrap() * gamma_j; + } + } + + // Q(x) over bhc is assumed to be zero, as checked in the test 'test_compute_Q' + assert_ne!(g_on_bhc, G::Scalar::ZERO); + + // evaluating g(x) over the boolean hypercube should give the same result as evaluating the + // sum of gamma^j * Lj(x) over the boolean hypercube + assert_eq!(g_on_bhc, sum_Lj_on_bhc); + + // evaluating g(x) over the boolean hypercube should give the same result as evaluating the + // sum of gamma^j * v_j over j \in [t] + assert_eq!(g_on_bhc, sum_v_j_gamma); + } + + fn test_compute_sigmas_and_thetas_with() { + let z1 = CCS::::get_test_z(3); + let z2 = CCS::::get_test_z(4); + + let (_, ccs_witness_1, ccs_instance_1, mles) = CCS::::gen_test_ccs(&z1); + let (ccs, ccs_witness_2, ccs_instance_2, _) = CCS::::gen_test_ccs(&z2); + let ck: CommitmentKey = ccs.commitment_key(); + + assert!(ccs.is_sat(&ck, &ccs_instance_1, &ccs_witness_1).is_ok()); + assert!(ccs.is_sat(&ck, &ccs_instance_2, &ccs_witness_2).is_ok()); + + let mut rng = OsRng; + let gamma: G::Scalar = G::Scalar::random(&mut rng); + let beta: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut rng)).collect(); + let r_x: Vec = (0..ccs.s).map(|_| G::Scalar::random(&mut OsRng)).collect(); + + let lcccs = LCCCS::new(&ccs, &mles, &ck, z1, r_x.clone()); + let cccs = CCCS::new(&ccs, &mles, z2, &ck); + + // Generate a new NIMFS instance + let nimfs = NIMFS::::new(ccs.clone(), mles.clone(), lcccs, ck.clone()); + let nimfs_witness = ccs_witness_1.w.clone(); + + let (sigmas, thetas) = + nimfs.compute_sigmas_and_thetas(&ccs_witness_2.w, &cccs, &nimfs_witness, &r_x); + + let g = nimfs.compute_g(&nimfs_witness, &cccs, &ccs_witness_2.w, gamma, &beta); + // Assert `g` is correctly computed here. + { + // evaluate g(x) over x \in {0,1}^s + let mut g_on_bhc = G::Scalar::ZERO; + for x in BooleanHypercube::new(ccs.s) { + g_on_bhc += g.evaluate(&x).unwrap(); + } + // evaluate sum_{j \in [t]} (gamma^j * Lj(x)) over x \in {0,1}^s + let mut sum_Lj_on_bhc = G::Scalar::ZERO; + let vec_L = nimfs.lcccs.compute_Ls(&ccs, &mles, &nimfs_witness); + for x in BooleanHypercube::new(ccs.s) { + for (j, coeff) in vec_L.iter().enumerate() { + let gamma_j = gamma.pow([j as u64]); + sum_Lj_on_bhc += coeff.evaluate(&x).unwrap() * gamma_j; + } + } + + // evaluating g(x) over the boolean hypercube should give the same result as evaluating the + // sum of gamma^j * Lj(x) over the boolean hypercube + assert_eq!(g_on_bhc, sum_Lj_on_bhc); + }; + + // XXX: We need a better way to do this. Sum_Mz has also the same issue. + // reverse the `r` given to evaluate to match Spartan/Nova endianness. + let mut reversed = r_x.clone(); + reversed.reverse(); + + // we expect g(r_x_prime) to be equal to: + // c = (sum gamma^j * e1 * sigma_j) + gamma^{t+1} * e2 * sum c_i * prod theta_j + // from `compute_c_from_sigmas_and_thetas` + let expected_c = g.evaluate(&reversed).unwrap(); + + let c = nimfs.compute_c_from_sigmas_and_thetas(&sigmas, &thetas, gamma, &beta, &r_x); + assert_eq!(c, expected_c); + } + + fn test_lccs_fold_with() { + let z1 = CCS::::get_test_z(3); + let z2 = CCS::::get_test_z(4); + + // ccs stays the same regardless of z1 or z2 + let (ccs, ccs_witness_1, ccs_instance_1, mles) = CCS::::gen_test_ccs(&z1); + let (_, ccs_witness_2, ccs_instance_2, _) = CCS::gen_test_ccs(&z2); + let ck: CommitmentKey = ccs.commitment_key(); + + assert!(ccs.is_sat(&ck, &ccs_instance_1, &ccs_witness_1).is_ok()); + assert!(ccs.is_sat(&ck, &ccs_instance_2, &ccs_witness_2).is_ok()); + + // Generate a new NIMFS instance + let mut nimfs = NIMFS::init(ccs.clone(), z1, b"Test NIMFS"); + let mut nimfs_witness = ccs_witness_1.w.clone(); + assert!(nimfs.is_sat(&nimfs_witness).is_ok()); + + // check folding correct stuff still alows the NIMFS to be satisfied correctly. + let cccs = nimfs.new_cccs(z2); + assert!(cccs.is_sat(&ccs, &mles, &ck, &ccs_witness_2.w).is_ok()); + + let (r_x_prime, rho) = nimfs.prepare_folding(); + let (sigmas, thetas) = + nimfs.compute_sigmas_and_thetas(&ccs_witness_2.w, &cccs, &nimfs_witness, &r_x_prime); + nimfs.fold(&cccs, sigmas, thetas, r_x_prime, rho); + nimfs.fold_witness(&cccs, &ccs_witness_2.w, &mut nimfs_witness, rho); + assert!(nimfs.is_sat(&nimfs_witness).is_ok()); + + // // Folding garbage should cause a failure + // let cccs = nimfs.new_cccs(vec![Fq::ONE, Fq::ONE, Fq::ONE]); + // nimfs.fold(&mut rng, cccs); + // assert!(nimfs.is_sat().is_err()); + // XXX: Should this indeed pass as it does now? + } + + #[test] + fn test_compute_g() { + test_compute_g_with::(); + } + + #[test] + fn test_compute_sigmas_and_thetas() { + test_compute_sigmas_and_thetas_with::() + } + + #[test] + fn test_lcccs_fold() { + test_lccs_fold_with::() + } +} diff --git a/src/ccs/util/mod.rs b/src/ccs/util/mod.rs new file mode 100644 index 000000000..02fd1af27 --- /dev/null +++ b/src/ccs/util/mod.rs @@ -0,0 +1,251 @@ +use crate::hypercube::BooleanHypercube; +use crate::spartan::math::Math; +use crate::spartan::polynomial::MultilinearPolynomial; +use crate::{ + constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_FE_FOR_RO, NUM_HASH_BITS}, + errors::NovaError, + gadgets::{ + nonnative::{bignat::nat_to_limbs, util::f_to_nat}, + utils::scalar_as_base, + }, + r1cs::{R1CSInstance, R1CSShape, R1CSWitness, R1CS}, + traits::{ + commitment::CommitmentEngineTrait, commitment::CommitmentTrait, AbsorbInROTrait, Group, ROTrait, + }, + utils::*, + Commitment, CommitmentKey, CE, +}; +use bitvec::vec; +use core::{cmp::max, marker::PhantomData}; +use ff::{Field, PrimeField}; +use itertools::concat; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use std::ops::{Add, Mul}; +use std::sync::Arc; + +use super::CCS; +pub(crate) mod virtual_poly; +pub(crate) use virtual_poly::VirtualPolynomial; + +/// Computes the MLE of the CCS's Matrix at index `j` and executes the reduction of it summing over the given z. +pub fn compute_sum_Mz( + M_mle: &MultilinearPolynomial, + z: &MultilinearPolynomial, +) -> MultilinearPolynomial { + let mut sum_Mz = MultilinearPolynomial::new(vec![ + G::Scalar::ZERO; + 1 << (M_mle.get_num_vars() - z.get_num_vars()) + ]); + + let bhc = BooleanHypercube::::new(z.get_num_vars()); + for y in bhc.into_iter() { + let M_y = fix_variables(M_mle, &y); + + // reverse y to match spartan/polynomial evaluate + let y_rev: Vec = y.into_iter().rev().collect(); + let z_y = z.evaluate(&y_rev); + let M_z = M_y.scalar_mul(&z_y); + // XXX: It's crazy to have results in the ops impls. Remove them! + sum_Mz = sum_Mz.clone().add(M_z).expect("This should not fail"); + } + + sum_Mz +} + +pub(crate) fn fix_variables( + poly: &MultilinearPolynomial, + partial_point: &[F], +) -> MultilinearPolynomial { + assert!( + partial_point.len() <= poly.get_num_vars(), + "invalid size of partial point" + ); + let nv = poly.get_num_vars(); + let mut poly = poly.Z.to_vec(); + let dim = partial_point.len(); + // evaluate single variable of partial point from left to right + for (i, point) in partial_point.iter().enumerate() { + poly = fix_one_variable_helper(&poly, nv - i, point); + } + + MultilinearPolynomial::::new(poly[..(1 << (nv - dim))].to_vec()) +} + +fn fix_one_variable_helper(data: &[F], nv: usize, point: &F) -> Vec { + let mut res = vec![F::ZERO; 1 << (nv - 1)]; + + for i in 0..(1 << (nv - 1)) { + res[i] = data[i << 1] + (data[(i << 1) + 1] - data[i << 1]) * point; + } + + res +} + +/// Return a vector of evaluations p_j(r) = \sum_{y \in {0,1}^s'} M_j(r, y) * z(y) +/// for all j values in 0..self.t +pub fn compute_all_sum_Mz_evals( + M_x_y_mle: &[MultilinearPolynomial], + z: &[G::Scalar], + r: &[G::Scalar], + s_prime: usize, +) -> Vec { + // Convert z to MLE + let z_y_mle = dense_vec_to_mle(s_prime, z); + + let mut v = Vec::with_capacity(M_x_y_mle.len()); + for M_i in M_x_y_mle { + let sum_Mz = compute_sum_Mz::(M_i, &z_y_mle); + + // XXX: We need a better way to do this. Sum_Mz has also the same issue. + // reverse the `r` given to evaluate to match Spartan/Nova endianness. + let mut r = r.to_vec(); + r.reverse(); + + let v_i = sum_Mz.evaluate(&r); + v.push(v_i); + } + v +} + +#[cfg(test)] +mod tests { + use crate::ccs::cccs::CCCS; + + use super::*; + use pasta_curves::{Ep, Fq}; + use rand_core::OsRng; + + fn test_fix_variables_with() { + let A = SparseMatrix::::with_coeffs( + 4, + 4, + vec![ + (0, 0, F::from(2u64)), + (0, 1, F::from(3u64)), + (0, 2, F::from(4u64)), + (0, 3, F::from(4u64)), + (1, 0, F::from(4u64)), + (1, 1, F::from(11u64)), + (1, 2, F::from(14u64)), + (1, 3, F::from(14u64)), + (2, 0, F::from(2u64)), + (2, 1, F::from(8u64)), + (2, 2, F::from(17u64)), + (2, 3, F::from(17u64)), + (3, 0, F::from(420u64)), + (3, 1, F::from(4u64)), + (3, 2, F::from(2u64)), + (3, 3, F::ZERO), + ], + ); + + let A_mle = A.to_mle(); + let bhc = BooleanHypercube::::new(2); + for (i, y) in bhc.enumerate() { + let A_mle_op = fix_variables(&A_mle, &y); + + // Check that fixing first variables pins down a column + // i.e. fixing x to 0 will return the first column + // fixing x to 1 will return the second column etc. + let column_i: Vec = A + .clone() + .coeffs() + .iter() + .copied() + .filter_map(|(_, col, coeff)| if col == i { Some(coeff) } else { None }) + .collect(); + + assert_eq!(A_mle_op.Z, column_i); + + // // Now check that fixing last variables pins down a row + // // i.e. fixing y to 0 will return the first row + // // fixing y to 1 will return the second row etc. + let row_i: Vec = A + .clone() + .coeffs() + .iter() + .copied() + .filter_map(|(row, _, coeff)| if row == i { Some(coeff) } else { None }) + .collect(); + + let mut last_vars_fixed = A_mle.clone(); + // this is equivalent to Espresso/hyperplonk's 'fix_last_variables' mehthod + for bit in y.clone().iter().rev() { + last_vars_fixed.bound_poly_var_top(bit) + } + + assert_eq!(last_vars_fixed.Z, row_i); + } + } + + fn test_compute_sum_Mz_over_boolean_hypercube_with() { + let z = CCS::::get_test_z(3); + let (ccs, _, _, mles) = CCS::::gen_test_ccs(&z); + + // Generate other artifacts + let ck = CCS::::commitment_key(&ccs); + let z_mle = dense_vec_to_mle(ccs.s_prime, &z); + let cccs = CCCS::new(&ccs, &mles, z, &ck); + + // check that evaluating over all the values x over the boolean hypercube, the result of + // the next for loop is equal to 0 + let mut r = G::Scalar::ZERO; + let bch = BooleanHypercube::new(ccs.s); + for x in bch.into_iter() { + for i in 0..ccs.q { + let mut Sj_prod = G::Scalar::ONE; + for j in ccs.S[i].clone() { + let sum_Mz: MultilinearPolynomial = compute_sum_Mz::(&mles[j], &z_mle); + let sum_Mz_x = sum_Mz.evaluate(&x); + Sj_prod *= sum_Mz_x; + } + r += Sj_prod * ccs.c[i]; + } + assert_eq!(r, G::Scalar::ZERO); + } + } + + fn test_compute_all_sum_Mz_evals_with() { + let z = CCS::::get_test_z(3); + let (ccs, _, _, mles) = CCS::::gen_test_ccs(&z); + + let mut r = vec![G::Scalar::ONE, G::Scalar::ZERO]; + let res = compute_all_sum_Mz_evals::(&mles, z.as_slice(), &r, ccs.s_prime); + assert_eq!( + res, + vec![ + G::Scalar::from(9u64), + G::Scalar::from(3u64), + G::Scalar::from(27u64) + ] + ); + + r.reverse(); + let res = compute_all_sum_Mz_evals::(&mles, z.as_slice(), &r, ccs.s_prime); + assert_eq!( + res, + vec![ + G::Scalar::from(30u64), + G::Scalar::from(1u64), + G::Scalar::from(30u64) + ] + ) + } + + #[test] + fn test_fix_variables() { + test_fix_variables_with::(); + } + + #[test] + fn test_compute_sum_Mz_over_boolean_hypercube() { + test_compute_sum_Mz_over_boolean_hypercube_with::(); + } + + #[test] + fn test_compute_all_sum_Mz_evals() { + test_compute_all_sum_Mz_evals_with::(); + } +} diff --git a/src/ccs/util/virtual_poly.rs b/src/ccs/util/virtual_poly.rs new file mode 100644 index 000000000..c48226469 --- /dev/null +++ b/src/ccs/util/virtual_poly.rs @@ -0,0 +1,508 @@ +use crate::hypercube::BooleanHypercube; +use crate::spartan::math::Math; +use crate::spartan::polynomial::{EqPolynomial, MultilinearPolynomial}; +use crate::{ + constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_FE_FOR_RO, NUM_HASH_BITS}, + errors::NovaError, + gadgets::{ + nonnative::{bignat::nat_to_limbs, util::f_to_nat}, + utils::scalar_as_base, + }, + r1cs::{R1CSInstance, R1CSShape, R1CSWitness, R1CS}, + traits::{ + commitment::CommitmentEngineTrait, commitment::CommitmentTrait, AbsorbInROTrait, Group, ROTrait, + }, + utils::*, + Commitment, CommitmentKey, CE, +}; +use bitvec::vec; +use core::{cmp::max, marker::PhantomData}; +use ff::{Field, PrimeField}; +use itertools::concat; +use rand::Rng; +use rand_core::RngCore; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use std::collections::HashMap; +use std::ops::{Add, Mul}; +use std::sync::Arc; + +// NOTE: This is a temporary solution to have multilinear polynomial product+addition. +// The idea is to re-evaluate once everything works and decide if we replace this code +// by something else. +// +// THIS CODE HAS BEEN TAKEN FROM THE ESPRESSO SYSTEMS LIB AND ADAPTED TO OUR NEEDS.: +// +// +#[rustfmt::skip] +/// A virtual polynomial is a sum of products of multilinear polynomials; +/// where the multilinear polynomials are stored via their multilinear +/// extensions: `(coefficient, DenseMultilinearExtension)` +/// +/// * Number of products n = `polynomial.products.len()`, +/// * Number of multiplicands of ith product m_i = +/// `polynomial.products[i].1.len()`, +/// * Coefficient of ith product c_i = `polynomial.products[i].0` +/// +/// The resulting polynomial is +/// +/// $$ \sum_{i=0}^{n} c_i \cdot \prod_{j=0}^{m_i} P_{ij} $$ +/// +/// Example: +/// f = c0 * f0 * f1 * f2 + c1 * f3 * f4 +/// where f0 ... f4 are multilinear polynomials +/// +/// - flattened_ml_extensions stores the multilinear extension representation of +/// f0, f1, f2, f3 and f4 +/// - products is +/// \[ +/// (c0, \[0, 1, 2\]), +/// (c1, \[3, 4\]) +/// \] +/// - raw_pointers_lookup_table maps fi to i +/// +#[derive(Clone, Debug, Default, PartialEq)] +pub struct VirtualPolynomial { + /// Aux information about the multilinear polynomial + pub aux_info: VPAuxInfo, + /// list of reference to products (as usize) of multilinear extension + pub products: Vec<(F, Vec)>, + /// Stores multilinear extensions in which product multiplicand can refer + /// to. + pub flattened_ml_extensions: Vec>>, + /// Pointers to the above poly extensions + raw_pointers_lookup_table: HashMap<*const MultilinearPolynomial, usize>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +/// Auxiliary information about the multilinear polynomial +pub struct VPAuxInfo { + /// max number of multiplicands in each product + pub max_degree: usize, + /// number of variables of the polynomial + pub num_variables: usize, + /// Associated field + #[doc(hidden)] + pub phantom: PhantomData, +} + +impl Add for &VirtualPolynomial { + type Output = VirtualPolynomial; + fn add(self, other: &VirtualPolynomial) -> Self::Output { + let mut res = self.clone(); + for products in other.products.iter() { + let cur: Vec>> = products + .1 + .iter() + .map(|&x| other.flattened_ml_extensions[x].clone()) + .collect(); + + res + .add_mle_list(cur, products.0) + .expect("add product failed"); + } + res + } +} + +// TODO: convert this into a trait +impl VirtualPolynomial { + /// Creates an empty virtual polynomial with `num_variables`. + pub fn new(num_variables: usize) -> Self { + VirtualPolynomial { + aux_info: VPAuxInfo { + max_degree: 0, + num_variables, + phantom: PhantomData, + }, + products: Vec::new(), + flattened_ml_extensions: Vec::new(), + raw_pointers_lookup_table: HashMap::new(), + } + } + + /// Creates an new virtual polynomial Fpom a MLE and its coefficient. + pub fn new_from_mle(mle: &Arc>, coefficient: F) -> Self { + let mle_ptr: *const MultilinearPolynomial = Arc::as_ptr(mle); + let mut hm = HashMap::new(); + hm.insert(mle_ptr, 0); + + VirtualPolynomial { + aux_info: VPAuxInfo { + // The max degree is the max degree of any individual variable + max_degree: 1, + num_variables: mle.get_num_vars(), + phantom: PhantomData, + }, + // here `0` points to the first polynomial of `flattened_ml_extensions` + products: vec![(coefficient, vec![0])], + flattened_ml_extensions: vec![mle.clone()], + raw_pointers_lookup_table: hm, + } + } + + /// Add a product of list of multilinear extensions to self + /// Returns an error if the list is empty, or the MLE has a different + /// `num_vars` Fpom self. + /// + /// The MLEs will be multiplied together, and then multiplied by the scalar + /// `coefficient`. + pub fn add_mle_list( + &mut self, + mle_list: impl IntoIterator>>, + coefficient: F, + ) -> Result<(), NovaError> { + let mle_list: Vec>> = mle_list.into_iter().collect(); + let mut indexed_product = Vec::with_capacity(mle_list.len()); + + if mle_list.is_empty() { + return Err(NovaError::VpArith); + } + + self.aux_info.max_degree = max(self.aux_info.max_degree, mle_list.len()); + + for mle in mle_list { + if mle.get_num_vars() != self.aux_info.num_variables { + return Err(NovaError::VpArith); + } + + let mle_ptr: *const MultilinearPolynomial = Arc::as_ptr(&mle); + if let Some(index) = self.raw_pointers_lookup_table.get(&mle_ptr) { + indexed_product.push(*index) + } else { + let curr_index = self.flattened_ml_extensions.len(); + self.flattened_ml_extensions.push(mle.clone()); + self.raw_pointers_lookup_table.insert(mle_ptr, curr_index); + indexed_product.push(curr_index); + } + } + self.products.push((coefficient, indexed_product)); + Ok(()) + } + + /// Multiple the current VirtualPolynomial by an MLE: + /// - add the MLE to the MLE list; + /// - multiple each product by MLE and its coefficient. + /// Returns an error if the MLE has a different `num_vars` Fpom self. + pub fn mul_by_mle( + &mut self, + mle: Arc>, + coefficient: F, + ) -> Result<(), NovaError> { + if mle.get_num_vars() != self.aux_info.num_variables { + return Err(NovaError::VpArith); + } + + let mle_ptr: *const MultilinearPolynomial = Arc::as_ptr(&mle); + + // check if this mle already exists in the virtual polynomial + let mle_index = match self.raw_pointers_lookup_table.get(&mle_ptr) { + Some(&p) => p, + None => { + self + .raw_pointers_lookup_table + .insert(mle_ptr, self.flattened_ml_extensions.len()); + self.flattened_ml_extensions.push(mle); + self.flattened_ml_extensions.len() - 1 + } + }; + + for (prod_coef, indices) in self.products.iter_mut() { + // - add the MLE to the MLE list; + // - multiple each product by MLE and its coefficient. + indices.push(mle_index); + *prod_coef *= coefficient; + } + + // increase the max degree by one as the MLE has degree 1. + self.aux_info.max_degree += 1; + + Ok(()) + } + + /// Given virtual polynomial `p(x)` and scalar `s`, compute `s*p(x)` + pub fn scalar_mul(&mut self, s: &F) { + for (prod_coef, _) in self.products.iter_mut() { + *prod_coef *= s; + } + } + + /// Evaluate the virtual polynomial at point `point`. + /// Returns an error is point.len() does not match `num_variables`. + pub fn evaluate(&self, point: &[F]) -> Result { + if self.aux_info.num_variables != point.len() { + return Err(NovaError::VpArith); + } + + // Evaluate all the MLEs at `point` + let evals: Vec = self + .flattened_ml_extensions + .iter() + .map(|x| x.evaluate(point)) + .collect(); + + let res = self + .products + .iter() + .map(|(c, p)| *c * p.iter().map(|&i| evals[i]).product::()) + .sum(); + + Ok(res) + } + + /// Sample a random virtual polynomial, return the polynomial and its sum. + pub fn rand( + nv: usize, + num_multiplicands_range: (usize, usize), + num_products: usize, + mut rng: &mut R, + ) -> Result<(Self, F), NovaError> { + let mut sum = F::ZERO; + let mut poly = VirtualPolynomial::new(nv); + for _ in 0..num_products { + let coefficient = F::random(&mut rng); + let num_multiplicands = rng.gen_range(num_multiplicands_range.0..num_multiplicands_range.1); + let (product, product_sum) = random_mle_list(nv, num_multiplicands, rng); + + poly.add_mle_list(product.into_iter(), coefficient)?; + sum += product_sum * coefficient; + } + Ok((poly, sum)) + } + + /// Sample a random virtual polynomial that evaluates to zero everywhere + /// over the boolean hypercube. + pub fn rand_zero( + nv: usize, + num_multiplicands_range: (usize, usize), + num_products: usize, + mut rng: &mut R, + ) -> Result { + let coefficient = F::random(&mut rng); + let mut poly = VirtualPolynomial::new(nv); + for _ in 0..num_products { + let num_multiplicands = rng.gen_range(num_multiplicands_range.0..num_multiplicands_range.1); + let product = random_zero_mle_list(nv, num_multiplicands, rng); + + poly.add_mle_list(product.into_iter(), coefficient)?; + } + + Ok(poly) + } + + // Input poly f(x) and a random vector r, output + // \hat f(x) = \sum_{x_i \in eval_x} f(x_i) eq(x, r) + // where + // eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) + // + // This function is used in ZeroCheck. + pub fn build_f_hat(&self, r: &[F]) -> Result { + if self.aux_info.num_variables != r.len() { + return Err(NovaError::VpArith); + } + + let eq_x_r = build_eq_x_r(r)?; + + let mut res = self.clone(); + res.mul_by_mle(eq_x_r, F::ONE)?; + + Ok(res) + } +} + +/// This function build the eq(x, r) polynomial for any given r. +/// +/// Evaluate +/// eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) +/// over r, which is +/// eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) +pub fn build_eq_x_r(r: &[F]) -> Result>, NovaError> { + let eq_polynomial = EqPolynomial::new(r.to_vec()); + let mut evaluations = eq_polynomial.evals(); + + // Re-orders the evaluations to match endianness of VirtualPoly + // + // NOTE: We probably want to benchmark this, + // but given that numbers of evaluations is small it might not be that bad + let permutation = generate_permutation(evaluations.len()); + reorder_vector(&mut evaluations, &permutation); + + let mle = MultilinearPolynomial::new(evaluations); + + Ok(Arc::new(mle)) +} + +/// Generates a permutation vector for a size `n` by reversing binary indices. +fn generate_permutation(n: usize) -> Vec { + (0..n) + .map(|i| { + let log_n = (n as f64).log2() as usize; // number of bits needed for the index + let mut res = 0; + for j in 0..log_n { + let bit = (i >> j) & 1; + res |= bit << (log_n - 1 - j); + } + res + }) + .collect() +} + +/// Reorders a vector based on a given permutation vector. +fn reorder_vector(vec: &mut Vec, permutation: &[usize]) { + let mut temp = vec.clone(); + for (i, &index) in permutation.iter().enumerate() { + temp[i] = vec[index].clone(); + } + *vec = temp; +} + +#[cfg(test)] +mod test { + use super::*; + use crate::hypercube::bit_decompose; + use pasta_curves::Fp; + use rand_core::OsRng; + + #[test] + fn test_virtual_polynomial_additions() -> Result<(), NovaError> { + let mut rng = OsRng; + for nv in 2..5 { + for num_products in 2..5 { + let base: Vec = (0..nv).map(|_| Fp::random(&mut rng)).collect(); + + let (a, _a_sum) = VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; + let (b, _b_sum) = VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; + let c = &a + &b; + + assert_eq!( + a.evaluate(base.as_ref())? + b.evaluate(base.as_ref())?, + c.evaluate(base.as_ref())? + ); + } + } + + Ok(()) + } + + #[test] + fn test_virtual_polynomial_mul_by_mle() -> Result<(), NovaError> { + let mut rng = OsRng; + for nv in 2..5 { + for num_products in 2..5 { + let base: Vec = (0..nv).map(|_| Fp::random(&mut rng)).collect(); + + let (a, _a_sum) = VirtualPolynomial::::rand(nv, (2, 3), num_products, &mut rng)?; + let (b, _b_sum) = random_mle_list(nv, 1, &mut rng); + let b_mle = b[0].clone(); + let coeff = Fp::random(&mut rng); + let b_vp = VirtualPolynomial::new_from_mle(&b_mle, coeff); + + let mut c = a.clone(); + + c.mul_by_mle(b_mle, coeff)?; + + assert_eq!( + a.evaluate(base.as_ref())? * b_vp.evaluate(base.as_ref())?, + c.evaluate(base.as_ref())? + ); + } + } + + Ok(()) + } + + #[test] + fn test_eq_xr() { + let mut rng = OsRng; + for nv in 4..10 { + let r: Vec = (0..nv).map(|_| Fp::random(&mut rng)).collect(); + let eq_x_r = build_eq_x_r(r.as_ref()).unwrap(); + let eq_x_r2 = build_eq_x_r_for_test(r.as_ref()); + assert_eq!(eq_x_r, eq_x_r2); + } + } + + /// Naive method to build eq(x, r). + /// Only used for testing purpose. + // Evaluate + // eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) + // over r, which is + // eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) + fn build_eq_x_r_for_test(r: &[F]) -> Arc> { + // we build eq(x,r) Fpom its evaluations + // we want to evaluate eq(x,r) over x \in {0, 1}^num_vars + // for example, with num_vars = 4, x is a binary vector of 4, then + // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) + // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) + // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) + // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) + // .... + // 1 1 1 1 -> r0 * r1 * r2 * r3 + // we will need 2^num_var evaluations + + // First, we build array for {1 - r_i} + let one_minus_r: Vec = r.iter().map(|ri| F::ONE - ri).collect(); + + let num_var = r.len(); + let mut eval = vec![]; + + for i in 0..1 << num_var { + let mut current_eval = F::ONE; + let bit_sequence = bit_decompose(i, num_var); + + for (&bit, (ri, one_minus_ri)) in bit_sequence.iter().zip(r.iter().zip(one_minus_r.iter())) { + current_eval *= if bit { *ri } else { *one_minus_ri }; + } + eval.push(current_eval); + } + + let mle = MultilinearPolynomial::new(eval); + + Arc::new(mle) + } + + #[cfg(test)] + mod tests { + use super::*; + use pasta_curves::Fq; + use rand_core::OsRng; + + #[test] + fn test_generate_permutation() { + assert_eq!(generate_permutation(2), vec![0, 1]); + assert_eq!(generate_permutation(4), vec![0, 2, 1, 3]); + assert_eq!(generate_permutation(8), vec![0, 4, 2, 6, 1, 5, 3, 7]); + } + + #[test] + fn test_reorder_vector() { + let mut vec = vec![10, 20, 30, 40, 50, 60, 70, 80]; + let permutation = vec![0, 4, 2, 6, 1, 5, 3, 7]; + reorder_vector(&mut vec, &permutation); + assert_eq!(vec, vec![10, 50, 30, 70, 20, 60, 40, 80]); + } + + #[test] + fn test_build_f_hat() { + let mut rng = OsRng; + let num_vars = 3; // You can change this value according to your requirement + + // Create a VirtualPolynomial + let poly = VirtualPolynomial::::new(num_vars); + let r: Vec = (0..num_vars).map(|_| Fq::random(&mut rng)).collect(); + + // Test with correct input length + let result = poly.build_f_hat(&r); + assert!(result.is_ok(), "Failed with correct input length"); + + // Test with incorrect input length + let bad_r: Vec = (0..num_vars + 1).map(|_| Fq::random(&mut rng)).collect(); + let result = poly.build_f_hat(&bad_r); + assert!( + matches!(result, Err(NovaError::VpArith)), + "Did not fail with incorrect input length" + ); + } + } +} diff --git a/src/circuit.rs b/src/circuit.rs index 60744f0e2..426ce18a5 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -40,7 +40,7 @@ pub struct NovaAugmentedCircuitParams { } impl NovaAugmentedCircuitParams { - pub fn new(limb_width: usize, n_limbs: usize, is_primary_circuit: bool) -> Self { + pub const fn new(limb_width: usize, n_limbs: usize, is_primary_circuit: bool) -> Self { Self { limb_width, n_limbs, @@ -87,19 +87,19 @@ impl NovaAugmentedCircuitInputs { /// The augmented circuit F' in Nova that includes a step circuit F /// and the circuit for the verifier in Nova's non-interactive folding scheme -pub struct NovaAugmentedCircuit> { - params: NovaAugmentedCircuitParams, +pub struct NovaAugmentedCircuit<'a, G: Group, SC: StepCircuit> { + params: &'a NovaAugmentedCircuitParams, ro_consts: ROConstantsCircuit, inputs: Option>, - step_circuit: SC, // The function that is applied for each step + step_circuit: &'a SC, // The function that is applied for each step } -impl> NovaAugmentedCircuit { +impl<'a, G: Group, SC: StepCircuit> NovaAugmentedCircuit<'a, G, SC> { /// Create a new verification circuit for the input relaxed r1cs instances - pub fn new( - params: NovaAugmentedCircuitParams, + pub const fn new( + params: &'a NovaAugmentedCircuitParams, inputs: Option>, - step_circuit: SC, + step_circuit: &'a SC, ro_consts: ROConstantsCircuit, ) -> Self { Self { @@ -262,8 +262,8 @@ impl> NovaAugmentedCircuit { } } -impl> Circuit<::Base> - for NovaAugmentedCircuit +impl<'a, G: Group, SC: StepCircuit> Circuit<::Base> + for NovaAugmentedCircuit<'a, G, SC> { fn synthesize::Base>>( self, @@ -396,27 +396,19 @@ mod tests { G1: Group::Scalar>, G2: Group::Scalar>, { + let ttc1 = TrivialTestCircuit::default(); // Initialize the shape and ck for the primary - let circuit1: NovaAugmentedCircuit::Base>> = - NovaAugmentedCircuit::new( - primary_params.clone(), - None, - TrivialTestCircuit::default(), - ro_consts1.clone(), - ); + let circuit1: NovaAugmentedCircuit<'_, G2, TrivialTestCircuit<::Base>> = + NovaAugmentedCircuit::new(&primary_params, None, &ttc1, ro_consts1.clone()); let mut cs: ShapeCS = ShapeCS::new(); let _ = circuit1.synthesize(&mut cs); let (shape1, ck1) = cs.r1cs_shape(); assert_eq!(cs.num_constraints(), num_constraints_primary); + let ttc2 = TrivialTestCircuit::default(); // Initialize the shape and ck for the secondary - let circuit2: NovaAugmentedCircuit::Base>> = - NovaAugmentedCircuit::new( - secondary_params.clone(), - None, - TrivialTestCircuit::default(), - ro_consts2.clone(), - ); + let circuit2: NovaAugmentedCircuit<'_, G1, TrivialTestCircuit<::Base>> = + NovaAugmentedCircuit::new(&secondary_params, None, &ttc2, ro_consts2.clone()); let mut cs: ShapeCS = ShapeCS::new(); let _ = circuit2.synthesize(&mut cs); let (shape2, ck2) = cs.r1cs_shape(); @@ -434,13 +426,8 @@ mod tests { None, None, ); - let circuit1: NovaAugmentedCircuit::Base>> = - NovaAugmentedCircuit::new( - primary_params, - Some(inputs1), - TrivialTestCircuit::default(), - ro_consts1, - ); + let circuit1: NovaAugmentedCircuit<'_, G2, TrivialTestCircuit<::Base>> = + NovaAugmentedCircuit::new(&primary_params, Some(inputs1), &ttc1, ro_consts1); let _ = circuit1.synthesize(&mut cs1); let (inst1, witness1) = cs1.r1cs_instance_and_witness(&shape1, &ck1).unwrap(); // Make sure that this is satisfiable @@ -458,13 +445,8 @@ mod tests { Some(inst1), None, ); - let circuit2: NovaAugmentedCircuit::Base>> = - NovaAugmentedCircuit::new( - secondary_params, - Some(inputs2), - TrivialTestCircuit::default(), - ro_consts2, - ); + let circuit2: NovaAugmentedCircuit<'_, G1, TrivialTestCircuit<::Base>> = + NovaAugmentedCircuit::new(&secondary_params, Some(inputs2), &ttc2, ro_consts2); let _ = circuit2.synthesize(&mut cs2); let (inst2, witness2) = cs2.r1cs_instance_and_witness(&shape2, &ck2).unwrap(); // Make sure that it is satisfiable diff --git a/src/errors.rs b/src/errors.rs index 3a2eac21d..bcc578b6d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -53,4 +53,8 @@ pub enum NovaError { /// returned when the consistency with public IO and assignment used fails #[error("IncorrectWitness")] IncorrectWitness, + + /// Tmp error for VirtualPolynomial artih error + #[error("VpArith")] + VpArith, } diff --git a/src/gadgets/ecc.rs b/src/gadgets/ecc.rs index d24cc5d4a..f6c2d1dde 100644 --- a/src/gadgets/ecc.rs +++ b/src/gadgets/ecc.rs @@ -81,7 +81,7 @@ where } /// Returns coordinates associated with the point. - pub fn get_coordinates( + pub const fn get_coordinates( &self, ) -> ( &AllocatedNum, @@ -424,7 +424,7 @@ where } /// A gadget for scalar multiplication, optimized to use incomplete addition law. - /// The optimization here is analogous to https://github.com/arkworks-rs/r1cs-std/blob/6d64f379a27011b3629cf4c9cb38b7b7b695d5a0/src/groups/curves/short_weierstrass/mod.rs#L295, + /// The optimization here is analogous to , /// except we use complete addition law over affine coordinates instead of projective coordinates for the tail bits pub fn scalar_mul>( &self, @@ -570,7 +570,7 @@ where G: Group, { /// Creates a new AllocatedPointNonInfinity from the specified coordinates - pub fn new(x: AllocatedNum, y: AllocatedNum) -> Self { + pub const fn new(x: AllocatedNum, y: AllocatedNum) -> Self { Self { x, y } } @@ -610,7 +610,7 @@ where } /// Returns coordinates associated with the point. - pub fn get_coordinates(&self) -> (&AllocatedNum, &AllocatedNum) { + pub const fn get_coordinates(&self) -> (&AllocatedNum, &AllocatedNum) { (&self.x, &self.y) } diff --git a/src/gadgets/nonnative/bignat.rs b/src/gadgets/nonnative/bignat.rs index 9db484802..eb3144ef8 100644 --- a/src/gadgets/nonnative/bignat.rs +++ b/src/gadgets/nonnative/bignat.rs @@ -783,7 +783,9 @@ impl Polynomial { #[cfg(test)] mod tests { use super::*; - use bellperson::Circuit; + use bellperson::{gadgets::test::TestConstraintSystem, Circuit}; + use pasta_curves::pallas::Scalar; + use proptest::prelude::*; pub struct PolynomialMultiplier { pub a: Vec, @@ -818,4 +820,79 @@ mod tests { Ok(()) } } + + #[test] + fn test_polynomial_multiplier_circuit() { + let mut cs = TestConstraintSystem::::new(); + + let circuit = PolynomialMultiplier { + a: [1, 1, 1].iter().map(|i| Scalar::from_u128(*i)).collect(), + b: [1, 1].iter().map(|i| Scalar::from_u128(*i)).collect(), + }; + + circuit.synthesize(&mut cs).expect("synthesis failed"); + + if let Some(token) = cs.which_is_unsatisfied() { + eprintln!("Error: {} is unsatisfied", token); + } + } + + #[derive(Debug)] + pub struct BigNatBitDecompInputs { + pub n: BigInt, + } + + pub struct BigNatBitDecompParams { + pub limb_width: usize, + pub n_limbs: usize, + } + + pub struct BigNatBitDecomp { + inputs: Option, + params: BigNatBitDecompParams, + } + + impl Circuit for BigNatBitDecomp { + fn synthesize>(self, cs: &mut CS) -> Result<(), SynthesisError> { + let n = BigNat::alloc_from_nat( + cs.namespace(|| "n"), + || Ok(self.inputs.grab()?.n.clone()), + self.params.limb_width, + self.params.n_limbs, + )?; + n.decompose(cs.namespace(|| "decomp"))?; + Ok(()) + } + } + + proptest! { + + #![proptest_config(ProptestConfig { + cases: 10, // this test is costlier as max n gets larger + .. ProptestConfig::default() + })] + #[test] + fn test_big_nat_can_decompose(n in any::(), limb_width in 40u8..200) { + let n = n as usize; + + let n_limbs = if n == 0 { + 1 + } else { + (n - 1) / limb_width as usize + 1 + }; + + let circuit = BigNatBitDecomp { + inputs: Some(BigNatBitDecompInputs { + n: BigInt::from(n), + }), + params: BigNatBitDecompParams { + limb_width: limb_width as usize, + n_limbs, + }, + }; + let mut cs = TestConstraintSystem::::new(); + circuit.synthesize(&mut cs).expect("synthesis failed"); + prop_assert!(cs.is_satisfied()); + } + } } diff --git a/src/gadgets/nonnative/util.rs b/src/gadgets/nonnative/util.rs index e0cceee58..486270d25 100644 --- a/src/gadgets/nonnative/util.rs +++ b/src/gadgets/nonnative/util.rs @@ -69,7 +69,7 @@ pub struct Num { } impl Num { - pub fn new(value: Option, num: LinearCombination) -> Self { + pub const fn new(value: Option, num: LinearCombination) -> Self { Self { value, num } } pub fn alloc(mut cs: CS, value: F) -> Result diff --git a/src/hypercube.rs b/src/hypercube.rs new file mode 100644 index 000000000..373290233 --- /dev/null +++ b/src/hypercube.rs @@ -0,0 +1,171 @@ +//! This module defines basic types related to Boolean hypercubes. +#![allow(unused)] +use std::marker::PhantomData; + +/// There's some overlap with polynomial.rs. +use ff::PrimeField; +use itertools::Itertools; + +#[derive(Debug)] +pub(crate) struct BooleanHypercube { + pub(crate) n_vars: usize, + current: u64, + max: u64, + _f: PhantomData, +} + +impl BooleanHypercube { + pub(crate) fn new(n_vars: usize) -> Self { + Self { + _f: PhantomData::, + n_vars, + current: 0, + max: 2_u32.pow(n_vars as u32) as u64, + } + } + + /// returns the entry at given i (which is the big-endian bit representation of i) + pub(crate) fn evaluate_at_big(&self, i: usize) -> Vec { + assert!(i < self.max as usize); + let bits = bit_decompose((i) as u64, self.n_vars); + bits.iter().map(|&x| F::from(x as u64)).collect() + } + + /// returns the entry at given i (which is the little-endian bit representation of i) + pub(crate) fn evaluate_at_little(&self, i: usize) -> Vec { + assert!(i < self.max as usize); + let bits = bit_decompose((i) as u64, self.n_vars); + bits.iter().map(|&x| F::from(x as u64)).rev().collect() + } + + pub(crate) fn evaluate_at(&self, i: usize) -> Vec { + // This is what we are currently using + self.evaluate_at_little(i) + } +} + +impl Iterator for BooleanHypercube { + type Item = Vec; + + fn next(&mut self) -> Option { + if self.current >= self.max { + None + } else { + let bits = bit_decompose(self.current, self.n_vars); + let point: Vec = bits.iter().map(|&bit| Scalar::from(bit as u64)).collect(); + self.current += 1; + Some(point) + } + } +} + +/// Decompose an integer into a binary vector in little endian. +pub fn bit_decompose(input: u64, num_var: usize) -> Vec { + let mut res = Vec::with_capacity(num_var); + let mut i = input; + for _ in 0..num_var { + res.push(i & 1 == 1); + i >>= 1; + } + res +} + +#[cfg(test)] +mod tests { + use super::*; + use ff::Field; + use pasta_curves::Fq; + + fn test_evaluate_with() { + let poly = BooleanHypercube::::new(3); + + let point = 7usize; + // So, f(1, 1, 1) = 5. + assert_eq!(poly.evaluate_at(point), vec![F::ONE, F::ONE, F::ONE]); + } + + fn test_big_endian_eval_with() { + let mut hypercube = BooleanHypercube::::new(3); + + let expected_outputs = vec![ + vec![F::ZERO, F::ZERO, F::ZERO], + vec![F::ONE, F::ZERO, F::ZERO], + vec![F::ZERO, F::ONE, F::ZERO], + vec![F::ONE, F::ONE, F::ZERO], + vec![F::ZERO, F::ZERO, F::ONE], + vec![F::ONE, F::ZERO, F::ONE], + vec![F::ZERO, F::ONE, F::ONE], + vec![F::ONE, F::ONE, F::ONE], + ]; + + for (i, _) in expected_outputs + .iter() + .enumerate() + .take(hypercube.max as usize) + { + assert_eq!(hypercube.evaluate_at_big(i), expected_outputs[i]); + } + } + + fn test_big_endian_next_with() { + let mut hypercube = BooleanHypercube::::new(3); + + let expected_outputs = vec![ + vec![F::ZERO, F::ZERO, F::ZERO], + vec![F::ONE, F::ZERO, F::ZERO], + vec![F::ZERO, F::ONE, F::ZERO], + vec![F::ONE, F::ONE, F::ZERO], + vec![F::ZERO, F::ZERO, F::ONE], + vec![F::ONE, F::ZERO, F::ONE], + vec![F::ZERO, F::ONE, F::ONE], + vec![F::ONE, F::ONE, F::ONE], + ]; + + for expected_output in expected_outputs { + let actual_output = hypercube.next().unwrap(); + assert_eq!(actual_output, expected_output); + } + } + + fn test_little_endian_eval_with() { + let mut hypercube = BooleanHypercube::::new(3); + + let expected_outputs = vec![ + vec![F::ZERO, F::ZERO, F::ZERO], + vec![F::ZERO, F::ZERO, F::ONE], + vec![F::ZERO, F::ONE, F::ZERO], + vec![F::ZERO, F::ONE, F::ONE], + vec![F::ONE, F::ZERO, F::ZERO], + vec![F::ONE, F::ZERO, F::ONE], + vec![F::ONE, F::ONE, F::ZERO], + vec![F::ONE, F::ONE, F::ONE], + ]; + + for (i, _) in expected_outputs + .iter() + .enumerate() + .take(hypercube.max as usize) + { + assert_eq!(hypercube.evaluate_at_little(i), expected_outputs[i]); + } + } + + #[test] + fn test_evaluate() { + test_evaluate_with::(); + } + #[test] + fn test_big_endian_eval() { + test_big_endian_eval_with::(); + } + + #[test] + fn test_big_endian_next() { + test_big_endian_next_with::(); + } + + #[test] + fn test_little_endian_eval() { + test_little_endian_eval_with::(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 59391e7b5..78a0e1389 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ )] #![allow(non_snake_case)] #![allow(clippy::type_complexity)] +#![allow(clippy::upper_case_acronyms)] #![forbid(unsafe_code)] // private modules @@ -17,7 +18,6 @@ mod circuit; mod constants; mod nifs; mod r1cs; - // public modules pub mod errors; pub mod gadgets; @@ -25,11 +25,16 @@ pub mod provider; pub mod spartan; pub mod traits; -use crate::bellperson::{ - r1cs::{NovaShape, NovaWitness}, - shape_cs::ShapeCS, - solver::SatisfyingAssignment, -}; +// experimental modules +#[cfg(feature = "hypernova")] +pub mod ccs; +#[cfg(feature = "hypernova")] +mod hypercube; +#[cfg(feature = "hypernova")] +mod utils; + +pub use crate::bellperson::{r1cs::NovaShape, shape_cs::ShapeCS}; +use crate::bellperson::{r1cs::NovaWitness, solver::SatisfyingAssignment}; use ::bellperson::{Circuit, ConstraintSystem}; use circuit::{NovaAugmentedCircuit, NovaAugmentedCircuitInputs, NovaAugmentedCircuitParams}; use constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_FE_WITHOUT_IO_FOR_CRHF, NUM_HASH_BITS}; @@ -83,7 +88,7 @@ where C2: StepCircuit, { /// Create a new `PublicParams` - pub fn setup(c_primary: C1, c_secondary: C2) -> Self { + pub fn setup(c_primary: &C1, c_secondary: &C2) -> Self { let augmented_circuit_params_primary = NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, true); let augmented_circuit_params_secondary = @@ -100,8 +105,8 @@ where let ro_consts_circuit_secondary: ROConstantsCircuit = ROConstantsCircuit::::new(); // Initialize ck for the primary - let circuit_primary: NovaAugmentedCircuit = NovaAugmentedCircuit::new( - augmented_circuit_params_primary.clone(), + let circuit_primary: NovaAugmentedCircuit<'_, G2, C1> = NovaAugmentedCircuit::new( + &augmented_circuit_params_primary, None, c_primary, ro_consts_circuit_primary.clone(), @@ -111,8 +116,8 @@ where let (r1cs_shape_primary, ck_primary) = cs.r1cs_shape(); // Initialize ck for the secondary - let circuit_secondary: NovaAugmentedCircuit = NovaAugmentedCircuit::new( - augmented_circuit_params_secondary.clone(), + let circuit_secondary: NovaAugmentedCircuit<'_, G1, C2> = NovaAugmentedCircuit::new( + &augmented_circuit_params_secondary, None, c_secondary, ro_consts_circuit_secondary.clone(), @@ -146,7 +151,7 @@ where } /// Returns the number of constraints in the primary and secondary circuits - pub fn num_constraints(&self) -> (usize, usize) { + pub const fn num_constraints(&self) -> (usize, usize) { ( self.r1cs_shape_primary.num_cons, self.r1cs_shape_secondary.num_cons, @@ -154,7 +159,7 @@ where } /// Returns the number of variables in the primary and secondary circuits - pub fn num_variables(&self) -> (usize, usize) { + pub const fn num_variables(&self) -> (usize, usize) { ( self.r1cs_shape_primary.num_vars, self.r1cs_shape_secondary.num_vars, @@ -216,10 +221,10 @@ where None, ); - let circuit_primary: NovaAugmentedCircuit = NovaAugmentedCircuit::new( - pp.augmented_circuit_params_primary.clone(), + let circuit_primary: NovaAugmentedCircuit<'_, G2, C1> = NovaAugmentedCircuit::new( + &pp.augmented_circuit_params_primary, Some(inputs_primary), - c_primary.clone(), + c_primary, pp.ro_consts_circuit_primary.clone(), ); let _ = circuit_primary.synthesize(&mut cs_primary); @@ -239,10 +244,10 @@ where Some(u_primary.clone()), None, ); - let circuit_secondary: NovaAugmentedCircuit = NovaAugmentedCircuit::new( - pp.augmented_circuit_params_secondary.clone(), + let circuit_secondary: NovaAugmentedCircuit<'_, G1, C2> = NovaAugmentedCircuit::new( + &pp.augmented_circuit_params_secondary, Some(inputs_secondary), - c_secondary.clone(), + c_secondary, pp.ro_consts_circuit_secondary.clone(), ); let _ = circuit_secondary.synthesize(&mut cs_secondary); @@ -328,10 +333,10 @@ where Some(Commitment::::decompress(&nifs_secondary.comm_T)?), ); - let circuit_primary: NovaAugmentedCircuit = NovaAugmentedCircuit::new( - pp.augmented_circuit_params_primary.clone(), + let circuit_primary: NovaAugmentedCircuit<'_, G2, C1> = NovaAugmentedCircuit::new( + &pp.augmented_circuit_params_primary, Some(inputs_primary), - c_primary.clone(), + c_primary, pp.ro_consts_circuit_primary.clone(), ); let _ = circuit_primary.synthesize(&mut cs_primary); @@ -365,10 +370,10 @@ where Some(Commitment::::decompress(&nifs_primary.comm_T)?), ); - let circuit_secondary: NovaAugmentedCircuit = NovaAugmentedCircuit::new( - pp.augmented_circuit_params_secondary.clone(), + let circuit_secondary: NovaAugmentedCircuit<'_, G1, C2> = NovaAugmentedCircuit::new( + &pp.augmented_circuit_params_secondary, Some(inputs_secondary), - c_secondary.clone(), + c_secondary, pp.ro_consts_circuit_secondary.clone(), ); let _ = circuit_secondary.synthesize(&mut cs_secondary); @@ -865,7 +870,7 @@ mod tests { T1: StepCircuit, T2: StepCircuit, { - let pp = PublicParams::::setup(circuit1, circuit2); + let pp = PublicParams::::setup(&circuit1, &circuit2); let digest_str = pp .digest @@ -929,7 +934,7 @@ mod tests { G2, TrivialTestCircuit<::Scalar>, TrivialTestCircuit<::Scalar>, - >::setup(test_circuit1.clone(), test_circuit2.clone()); + >::setup(&test_circuit1, &test_circuit2); let num_steps = 1; @@ -985,7 +990,7 @@ mod tests { G2, TrivialTestCircuit<::Scalar>, CubicCircuit<::Scalar>, - >::setup(circuit_primary.clone(), circuit_secondary.clone()); + >::setup(&circuit_primary, &circuit_secondary); let num_steps = 3; @@ -1072,7 +1077,7 @@ mod tests { G2, TrivialTestCircuit<::Scalar>, CubicCircuit<::Scalar>, - >::setup(circuit_primary.clone(), circuit_secondary.clone()); + >::setup(&circuit_primary, &circuit_secondary); let num_steps = 3; @@ -1167,7 +1172,7 @@ mod tests { G2, TrivialTestCircuit<::Scalar>, CubicCircuit<::Scalar>, - >::setup(circuit_primary.clone(), circuit_secondary.clone()); + >::setup(&circuit_primary, &circuit_secondary); let num_steps = 3; @@ -1339,7 +1344,7 @@ mod tests { G2, FifthRootCheckingCircuit<::Scalar>, TrivialTestCircuit<::Scalar>, - >::setup(circuit_primary, circuit_secondary.clone()); + >::setup(&circuit_primary, &circuit_secondary); let num_steps = 3; @@ -1417,7 +1422,7 @@ mod tests { G2, TrivialTestCircuit<::Scalar>, CubicCircuit<::Scalar>, - >::setup(test_circuit1.clone(), test_circuit2.clone()); + >::setup(&test_circuit1, &test_circuit2); let num_steps = 1; diff --git a/src/provider/ipa_pc.rs b/src/provider/ipa_pc.rs index fa8068bbd..0ae536abc 100644 --- a/src/provider/ipa_pc.rs +++ b/src/provider/ipa_pc.rs @@ -177,7 +177,7 @@ where G: Group, CommitmentKey: CommitmentKeyExtTrait, { - fn protocol_name() -> &'static [u8] { + const fn protocol_name() -> &'static [u8] { b"IPA" } diff --git a/src/provider/pasta.rs b/src/provider/pasta.rs index de3c31be1..471ee3280 100644 --- a/src/provider/pasta.rs +++ b/src/provider/pasta.rs @@ -31,7 +31,7 @@ pub struct PallasCompressedElementWrapper { impl PallasCompressedElementWrapper { /// Wraps repr into the wrapper - pub fn new(repr: [u8; 32]) -> Self { + pub const fn new(repr: [u8; 32]) -> Self { Self { repr } } } @@ -44,7 +44,7 @@ pub struct VestaCompressedElementWrapper { impl VestaCompressedElementWrapper { /// Wraps repr into the wrapper - pub fn new(repr: [u8; 32]) -> Self { + pub const fn new(repr: [u8; 32]) -> Self { Self { repr } } } @@ -207,6 +207,15 @@ impl TranscriptReprTrait for pallas::Scalar { } } +impl TranscriptReprTrait for Vec { + fn to_transcript_bytes(&self) -> Vec { + self + .iter() + .flat_map(|scalar| scalar.to_transcript_bytes()) + .collect() + } +} + impl_traits!( pallas, PallasCompressedElementWrapper, diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index bad1247e9..fe00c52fd 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -203,7 +203,9 @@ impl CommitmentEngineTrait for CommitmentEngine { } } -pub(crate) trait CommitmentKeyExtTrait { +/// A trait listing properties of a commitment key that can be managed in a divide-and-conquer fashion +pub trait CommitmentKeyExtTrait { + /// Holds the type of the commitment engine type CE: CommitmentEngineTrait; /// Splits the commitment key into two pieces at a specified point diff --git a/src/r1cs.rs b/src/r1cs.rs index 4b204e9eb..105101ec7 100644 --- a/src/r1cs.rs +++ b/src/r1cs.rs @@ -119,10 +119,16 @@ impl R1CSShape { } // We require the number of public inputs/outputs to be even + #[cfg(not(feature = "hypernova"))] if num_io % 2 != 0 { return Err(NovaError::OddInputLength); } + // TODO: See https://github.com/privacy-scaling-explorations/Nova/issues/30 + #[cfg(feature = "hypernova")] + // if num_io % 2 != 0 { + // return Err(NovaError::OddInputLength); + // } Ok(R1CSShape { num_cons, num_vars, @@ -133,6 +139,16 @@ impl R1CSShape { }) } + // Checks regularity conditions on the R1CSShape, required in Spartan-class SNARKs + // Panics if num_cons, num_vars, or num_io are not powers of two, or if num_io > num_vars + #[inline] + pub(crate) fn check_regular_shape(&self) { + assert_eq!(self.num_cons.next_power_of_two(), self.num_cons); + assert_eq!(self.num_vars.next_power_of_two(), self.num_vars); + assert_eq!(self.num_io.next_power_of_two(), self.num_io); + assert!(self.num_io < self.num_vars); + } + pub fn multiply_vec( &self, z: &[G::Scalar], diff --git a/src/spartan/mod.rs b/src/spartan/mod.rs index a8ef0223c..23a35f710 100644 --- a/src/spartan/mod.rs +++ b/src/spartan/mod.rs @@ -4,8 +4,8 @@ //! and another in ppsnark.rs (which uses preprocessing to keep the verifier's state small if the PCS scheme provides a succinct verifier) //! We also provide direct.rs that allows proving a step circuit directly with either of the two SNARKs. pub mod direct; -mod math; -pub(crate) mod polynomial; +pub(crate) mod math; +pub mod polynomial; pub mod ppsnark; pub mod snark; mod sumcheck; diff --git a/src/spartan/polynomial.rs b/src/spartan/polynomial.rs index 18387f19f..ee8ecba54 100644 --- a/src/spartan/polynomial.rs +++ b/src/spartan/polynomial.rs @@ -1,20 +1,46 @@ -//! This module defines basic types related to polynomials +//! This module provides foundational types and functions for manipulating multilinear polynomials in the context of cryptographic computations. +//! +//! Main components: +//! - `EqPolynomial`: Represents multilinear extension of equality polynomials, evaluated based on binary input values. +//! - `MultilinearPolynomial`: Dense representation of multilinear polynomials, represented by evaluations over all possible binary inputs. +//! - `SparsePolynomial`: Efficient representation of sparse multilinear polynomials, storing only non-zero evaluations. use core::ops::Index; use ff::PrimeField; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use std::ops::{Add, Mul}; -pub(crate) struct EqPolynomial { +use crate::spartan::math::Math; + +/// Represents the multilinear extension polynomial (MLE) of the equality polynomial $eq(x,e)$, denoted as $\tilde{eq}(x, e)$. +/// +/// The polynomial is defined by the formula: +/// $$ +/// \tilde{eq}(x, e) = \prod_{i=0}^m(e_i * x_i + (1 - e_i) * (1 - x_i)) +/// $$ +/// +/// Each element in the vector `r` corresponds to a component $e_i$, representing a bit from the binary representation of an input value $e$. +/// This polynomial evaluates to 1 if every component $x_i$ equals its corresponding $e_i$, and 0 otherwise. +/// +/// For instance, for e = 6 (with a binary representation of 0b110), the vector r would be [1, 1, 0]. +pub struct EqPolynomial { r: Vec, } impl EqPolynomial { - /// Creates a new polynomial from its succinct specification - pub fn new(r: Vec) -> Self { + /// Creates a new `EqPolynomial` from a vector of Scalars `r`. + /// + /// Each Scalar in `r` corresponds to a bit from the binary representation of an input value `e`. + pub const fn new(r: Vec) -> Self { EqPolynomial { r } } - /// Evaluates the polynomial at the specified point + /// Evaluates the `EqPolynomial` at a given point `rx`. + /// + /// This function computes the value of the polynomial at the point specified by `rx`. + /// It expects `rx` to have the same length as the internal vector `r`. + /// + /// Panics if `rx` and `r` have different lengths. pub fn evaluate(&self, rx: &[Scalar]) -> Scalar { assert_eq!(self.r.len(), rx.len()); (0..rx.len()) @@ -22,6 +48,9 @@ impl EqPolynomial { .fold(Scalar::ONE, |acc, item| acc * item) } + /// Evaluates the `EqPolynomial` at all the `2^|r|` points in its domain. + /// + /// Returns a vector of Scalars, each corresponding to the polynomial evaluation at a specific point. pub fn evals(&self) -> Vec { let ell = self.r.len(); let mut evals: Vec = vec![Scalar::ZERO; (2_usize).pow(ell as u32)]; @@ -42,17 +71,37 @@ impl EqPolynomial { size *= 2; } + evals } } -#[derive(Debug, Clone, Serialize, Deserialize)] +/// A multilinear extension of a polynomial $Z(\cdot)$, denote it as $\tilde{Z}(x_1, ..., x_m)$ +/// where the degree of each variable is at most one. +/// +/// This is the dense representation of a multilinear poynomial. +/// Let it be $\mathbb{G}(\cdot): \mathbb{F}^m \rightarrow \mathbb{F}$, it can be represented uniquely by the list of +/// evaluations of $\mathbb{G}(\cdot)$ over the Boolean hypercube $\{0, 1\}^m$. +/// +/// For example, a 3 variables multilinear polynomial can be represented by evaluation +/// at points $[0, 2^3-1]$. +/// +/// The implementation follows +/// $$ +/// \tilde{Z}(x_1, ..., x_m) = \sum_{e\in {0,1}^m}Z(e)\cdot \prod_{i=0}^m(x_i\cdot e_i)\cdot (1-e_i) +/// $$ +/// +/// Vector $Z$ indicates $Z(e)$ where $e$ ranges from $0$ to $2^m-1$. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MultilinearPolynomial { - num_vars: usize, // the number of variables in the multilinear polynomial - Z: Vec, // evaluations of the polynomial in all the 2^num_vars Boolean inputs + num_vars: usize, // the number of variables in the multilinear polynomial + pub(crate) Z: Vec, // evaluations of the polynomial in all the 2^num_vars Boolean inputs } impl MultilinearPolynomial { + /// Creates a new MultilinearPolynomial from the given evaluations. + /// + /// The number of evaluations must be a power of two. pub fn new(Z: Vec) -> Self { assert_eq!(Z.len(), (2_usize).pow((Z.len() as f64).log2() as u32)); MultilinearPolynomial { @@ -61,14 +110,26 @@ impl MultilinearPolynomial { } } - pub fn get_num_vars(&self) -> usize { + /// Returns the number of variables in the multilinear polynomial + pub const fn get_num_vars(&self) -> usize { self.num_vars } + /// Returns the total number of evaluations. pub fn len(&self) -> usize { self.Z.len() } + /// Checks if the multilinear polynomial is empty. + /// + /// This method returns true if the polynomial has no evaluations, and false otherwise. + pub fn is_empty(&self) -> bool { + self.Z.is_empty() + } + + /// Bounds the polynomial's top variable using the given scalar. + /// + /// This operation modifies the polynomial in-place. pub fn bound_poly_var_top(&mut self, r: &Scalar) { let n = self.len() / 2; @@ -86,7 +147,10 @@ impl MultilinearPolynomial { self.num_vars -= 1; } - // returns Z(r) in O(n) time + /// Evaluates the polynomial at the given point. + /// Returns Z(r) in O(n) time. + /// + /// The point must have a value for each variable. pub fn evaluate(&self, r: &[Scalar]) -> Scalar { // r must have a value for each variable assert_eq!(r.len(), self.get_num_vars()); @@ -96,9 +160,10 @@ impl MultilinearPolynomial { (0..chis.len()) .into_par_iter() .map(|i| chis[i] * self.Z[i]) - .reduce(|| Scalar::ZERO, |x, y| x + y) + .sum() } + /// Evaluates the polynomial with the given evaluations and point. pub fn evaluate_with(Z: &[Scalar], r: &[Scalar]) -> Scalar { EqPolynomial::new(r.to_vec()) .evals() @@ -107,6 +172,16 @@ impl MultilinearPolynomial { .map(|(a, b)| a * b) .reduce(|| Scalar::ZERO, |x, y| x + y) } + + /// Multiplies the polynomial by a scalar. + #[allow(unused)] + pub fn scalar_mul(&self, scalar: &Scalar) -> Self { + let mut new_poly = self.clone(); + for z in &mut new_poly.Z { + *z *= scalar; + } + new_poly + } } impl Index for MultilinearPolynomial { @@ -118,6 +193,12 @@ 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. pub(crate) struct SparsePolynomial { num_vars: usize, Z: Vec<(usize, Scalar)>, @@ -128,6 +209,8 @@ impl SparsePolynomial { SparsePolynomial { 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; @@ -145,19 +228,182 @@ impl SparsePolynomial { pub fn evaluate(&self, r: &[Scalar]) -> Scalar { assert_eq!(self.num_vars, r.len()); - let get_bits = |num: usize, num_bits: usize| -> Vec { - (0..num_bits) - .into_par_iter() - .map(|shift_amount| ((num & (1 << (num_bits - shift_amount - 1))) > 0)) - .collect::>() - }; - (0..self.Z.len()) .into_par_iter() .map(|i| { - let bits = get_bits(self.Z[i].0, r.len()); + let bits = (self.Z[i].0).get_bits(r.len()); SparsePolynomial::compute_chi(&bits, r) * self.Z[i].1 }) .reduce(|| Scalar::ZERO, |x, y| x + y) } } + +/// Adds another multilinear polynomial to `self`. +/// Assumes the two polynomials have the same number of variables. +impl Add for MultilinearPolynomial { + type Output = Result; + + fn add(self, other: Self) -> Self::Output { + if self.get_num_vars() != other.get_num_vars() { + return Err("The two polynomials must have the same number of variables"); + } + + let sum: Vec = self + .Z + .iter() + .zip(other.Z.iter()) + .map(|(a, b)| *a + *b) + .collect(); + + Ok(MultilinearPolynomial::new(sum)) + } +} + +/// Multiplies `self` by another multilinear polynomial. +/// Assumes the two polynomials have the same number of variables. +impl Mul for MultilinearPolynomial { + type Output = Result; + + fn mul(self, other: Self) -> Self::Output { + if self.get_num_vars() != other.get_num_vars() { + return Err("The two polynomials must have the same number of variables"); + } + + let product: Vec = self + .Z + .iter() + .zip(other.Z.iter()) + .map(|(a, b)| *a * *b) + .collect(); + + Ok(MultilinearPolynomial::new(product)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pasta_curves::Fp; + + fn make_mlp(len: usize, value: F) -> MultilinearPolynomial { + MultilinearPolynomial { + num_vars: len.count_ones() as usize, + Z: vec![value; len], + } + } + + fn test_eq_polynomial_with() { + let eq_poly = EqPolynomial::::new(vec![F::ONE, F::ZERO, F::ONE]); + let y = eq_poly.evaluate(vec![F::ONE, F::ONE, F::ONE].as_slice()); + assert_eq!(y, F::ZERO); + + let y = eq_poly.evaluate(vec![F::ONE, F::ZERO, F::ONE].as_slice()); + assert_eq!(y, F::ONE); + + let eval_list = eq_poly.evals(); + for (i, &coeff) in eval_list.iter().enumerate().take((2_usize).pow(3)) { + if i == 5 { + assert_eq!(coeff, F::ONE); + } else { + assert_eq!(coeff, F::ZERO); + } + } + } + + fn test_multilinear_polynomial_with() { + // Let the polynomial has 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 TWO = F::from(2); + + let Z = vec![ + F::ZERO, + F::ZERO, + F::ZERO, + F::ONE, + F::ZERO, + F::ONE, + F::ZERO, + TWO, + ]; + let m_poly = MultilinearPolynomial::::new(Z.clone()); + assert_eq!(m_poly.get_num_vars(), 3); + + let x = vec![F::ONE, F::ONE, F::ONE]; + assert_eq!(m_poly.evaluate(x.as_slice()), TWO); + + let y = MultilinearPolynomial::::evaluate_with(Z.as_slice(), x.as_slice()); + assert_eq!(y, TWO); + } + + 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 TWO = F::from(2); + let Z = vec![(3, F::ONE), (5, F::ONE), (7, TWO)]; + let m_poly = SparsePolynomial::::new(3, Z); + + let x = vec![F::ONE, F::ONE, F::ONE]; + assert_eq!(m_poly.evaluate(x.as_slice()), TWO); + + let x = vec![F::ONE, F::ZERO, F::ONE]; + assert_eq!(m_poly.evaluate(x.as_slice()), F::ONE); + } + + #[test] + fn test_eq_polynomial() { + test_eq_polynomial_with::(); + } + + #[test] + fn test_multilinear_polynomial() { + test_multilinear_polynomial_with::(); + } + + #[test] + fn test_sparse_polynomial() { + test_sparse_polynomial_with::(); + } + + fn test_mlp_add_with() { + let mlp1 = make_mlp(4, F::from(3)); + let mlp2 = make_mlp(4, F::from(7)); + + let mlp3 = mlp1.add(mlp2).unwrap(); + + assert_eq!(mlp3.Z, vec![F::from(10); 4]); + } + + fn test_mlp_scalar_mul_with() { + let mlp = make_mlp(4, F::from(3)); + + let mlp2 = mlp.scalar_mul(&F::from(2)); + + assert_eq!(mlp2.Z, vec![F::from(6); 4]); + } + + fn test_mlp_mul_with() { + let mlp1 = make_mlp(4, F::from(3)); + let mlp2 = make_mlp(4, F::from(7)); + + let mlp3 = mlp1.mul(mlp2).unwrap(); + + assert_eq!(mlp3.Z, vec![F::from(21); 4]); + } + + #[test] + fn test_mlp_add() { + test_mlp_add_with::(); + } + + #[test] + fn test_mlp_scalar_mul() { + test_mlp_scalar_mul_with::(); + } + + #[test] + fn test_mlp_mul() { + test_mlp_mul_with::(); + } +} diff --git a/src/spartan/ppsnark.rs b/src/spartan/ppsnark.rs index 0011f463b..57bec8fe7 100644 --- a/src/spartan/ppsnark.rs +++ b/src/spartan/ppsnark.rs @@ -119,51 +119,34 @@ impl R1CSShapeSparkRepr { max(total_nz, max(2 * S.num_vars, S.num_cons)).next_power_of_two() }; - let row = { - let mut r = S - .A - .iter() - .chain(S.B.iter()) - .chain(S.C.iter()) - .map(|(r, _, _)| *r) - .collect::>(); - r.resize(N, 0usize); - r - }; + let (mut row, mut col) = (vec![0usize; N], vec![0usize; N]); - let col = { - let mut c = S - .A - .iter() - .chain(S.B.iter()) - .chain(S.C.iter()) - .map(|(_, c, _)| *c) - .collect::>(); - c.resize(N, 0usize); - c - }; + for (i, (r, c, _)) in S.A.iter().chain(S.B.iter()).chain(S.C.iter()).enumerate() { + row[i] = *r; + col[i] = *c; + } let val_A = { - let mut val = S.A.iter().map(|(_, _, v)| *v).collect::>(); - val.resize(N, G::Scalar::ZERO); + let mut val = vec![G::Scalar::ZERO; N]; + for (i, (_, _, v)) in S.A.iter().enumerate() { + val[i] = *v; + } val }; let val_B = { - // prepend zeros - let mut val = vec![G::Scalar::ZERO; S.A.len()]; - val.extend(S.B.iter().map(|(_, _, v)| *v).collect::>()); - // append zeros - val.resize(N, G::Scalar::ZERO); + let mut val = vec![G::Scalar::ZERO; N]; + for (i, (_, _, v)) in S.B.iter().enumerate() { + val[S.A.len() + i] = *v; + } val }; let val_C = { - // prepend zeros - let mut val = vec![G::Scalar::ZERO; S.A.len() + S.B.len()]; - val.extend(S.C.iter().map(|(_, _, v)| *v).collect::>()); - // append zeros - val.resize(N, G::Scalar::ZERO); + let mut val = vec![G::Scalar::ZERO; N]; + for (i, (_, _, v)) in S.C.iter().enumerate() { + val[S.A.len() + S.B.len() + i] = *v; + } val }; @@ -265,29 +248,30 @@ impl R1CSShapeSparkRepr { let mem_row = EqPolynomial::new(r_x_padded).evals(); let mem_col = { - let mut z = z.to_vec(); - z.resize(self.N, G::Scalar::ZERO); - z + let mut val = vec![G::Scalar::ZERO; self.N]; + for (i, v) in z.iter().enumerate() { + val[i] = *v; + } + val }; - let mut E_row = S - .A - .iter() - .chain(S.B.iter()) - .chain(S.C.iter()) - .map(|(r, _, _)| mem_row[*r]) - .collect::>(); - - let mut E_col = S - .A - .iter() - .chain(S.B.iter()) - .chain(S.C.iter()) - .map(|(_, c, _)| mem_col[*c]) - .collect::>(); + let (E_row, E_col) = { + let mut E_row = vec![mem_row[0]; self.N]; // we place mem_row[0] since resized row is appended with 0s + let mut E_col = vec![mem_col[0]; self.N]; - E_row.resize(self.N, mem_row[0]); // we place mem_row[0] since resized row is appended with 0s - E_col.resize(self.N, mem_col[0]); + for (i, (val_r, val_c)) in S + .A + .iter() + .chain(S.B.iter()) + .chain(S.C.iter()) + .map(|(r, c, _)| (mem_row[*r], mem_col[*c])) + .enumerate() + { + E_row[i] = val_r; + E_col[i] = val_c; + } + (E_row, E_col) + }; (mem_row, mem_col, E_row, E_col) } @@ -411,12 +395,10 @@ impl ProductSumcheckInstance { let poly_A = MultilinearPolynomial::new(EqPolynomial::new(rand_eq).evals()); let poly_B_vec = left_vec - .clone() .into_par_iter() .map(MultilinearPolynomial::new) .collect::>(); let poly_C_vec = right_vec - .clone() .into_par_iter() .map(MultilinearPolynomial::new) .collect::>(); @@ -477,43 +459,10 @@ impl SumcheckEngine for ProductSumcheckInstance { .zip(self.poly_C_vec.iter()) .zip(self.poly_D_vec.iter()) .map(|((poly_B, poly_C), poly_D)| { - let len = poly_B.len() / 2; // Make an iterator returning the contributions to the evaluations - let (eval_point_0, eval_point_2, eval_point_3) = (0..len) - .into_par_iter() - .map(|i| { - // eval 0: bound_func is A(low) - let eval_point_0 = comb_func(&poly_A[i], &poly_B[i], &poly_C[i], &poly_D[i]); - - // eval 2: bound_func is -A(low) + 2*A(high) - let poly_A_bound_point = poly_A[len + i] + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B[len + i] + poly_B[len + i] - poly_B[i]; - let poly_C_bound_point = poly_C[len + i] + poly_C[len + i] - poly_C[i]; - let poly_D_bound_point = poly_D[len + i] + poly_D[len + i] - poly_D[i]; - let eval_point_2 = comb_func( - &poly_A_bound_point, - &poly_B_bound_point, - &poly_C_bound_point, - &poly_D_bound_point, - ); - - // eval 3: bound_func is -2A(low) + 3A(high); computed incrementally with bound_func applied to eval(2) - let poly_A_bound_point = poly_A_bound_point + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B_bound_point + poly_B[len + i] - poly_B[i]; - let poly_C_bound_point = poly_C_bound_point + poly_C[len + i] - poly_C[i]; - let poly_D_bound_point = poly_D_bound_point + poly_D[len + i] - poly_D[i]; - let eval_point_3 = comb_func( - &poly_A_bound_point, - &poly_B_bound_point, - &poly_C_bound_point, - &poly_D_bound_point, - ); - (eval_point_0, eval_point_2, eval_point_3) - }) - .reduce( - || (G::Scalar::ZERO, G::Scalar::ZERO, G::Scalar::ZERO), - |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2), - ); + let (eval_point_0, eval_point_2, eval_point_3) = + SumcheckProof::::compute_eval_points_cubic(poly_A, poly_B, poly_C, poly_D, &comb_func); + vec![eval_point_0, eval_point_2, eval_point_3] }) .collect::>>() @@ -584,44 +533,10 @@ impl SumcheckEngine for OuterSumcheckInstance { poly_C_comp: &G::Scalar, poly_D_comp: &G::Scalar| -> G::Scalar { *poly_A_comp * (*poly_B_comp * *poly_C_comp - *poly_D_comp) }; - let len = poly_A.len() / 2; // Make an iterator returning the contributions to the evaluations - let (eval_point_0, eval_point_2, eval_point_3) = (0..len) - .into_par_iter() - .map(|i| { - // eval 0: bound_func is A(low) - let eval_point_0 = comb_func(&poly_A[i], &poly_B[i], &poly_C[i], &poly_D[i]); - - // eval 2: bound_func is -A(low) + 2*A(high) - let poly_A_bound_point = poly_A[len + i] + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B[len + i] + poly_B[len + i] - poly_B[i]; - let poly_C_bound_point = poly_C[len + i] + poly_C[len + i] - poly_C[i]; - let poly_D_bound_point = poly_D[len + i] + poly_D[len + i] - poly_D[i]; - let eval_point_2 = comb_func( - &poly_A_bound_point, - &poly_B_bound_point, - &poly_C_bound_point, - &poly_D_bound_point, - ); - - // eval 3: bound_func is -2A(low) + 3A(high); computed incrementally with bound_func applied to eval(2) - let poly_A_bound_point = poly_A_bound_point + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B_bound_point + poly_B[len + i] - poly_B[i]; - let poly_C_bound_point = poly_C_bound_point + poly_C[len + i] - poly_C[i]; - let poly_D_bound_point = poly_D_bound_point + poly_D[len + i] - poly_D[i]; - let eval_point_3 = comb_func( - &poly_A_bound_point, - &poly_B_bound_point, - &poly_C_bound_point, - &poly_D_bound_point, - ); - (eval_point_0, eval_point_2, eval_point_3) - }) - .reduce( - || (G::Scalar::ZERO, G::Scalar::ZERO, G::Scalar::ZERO), - |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2), - ); + let (eval_point_0, eval_point_2, eval_point_3) = + SumcheckProof::::compute_eval_points_cubic(poly_A, poly_B, poly_C, poly_D, &comb_func); vec![vec![eval_point_0, eval_point_2, eval_point_3]] } @@ -673,6 +588,8 @@ impl SumcheckEngine for InnerSumcheckInstance { -> G::Scalar { *poly_A_comp * *poly_B_comp * *poly_C_comp }; let len = poly_A.len() / 2; + // TODO: make this call a function in sumcheck.rs by writing an n-ary variant of crate::spartan::sumcheck::SumcheckProof::::compute_eval_points_cubic + // once #[feature(array_methods)] stabilizes (this n-ary variant would need array::each_ref) // Make an iterator returning the contributions to the evaluations let (eval_point_0, eval_point_2, eval_point_3) = (0..len) .into_par_iter() @@ -862,7 +779,7 @@ impl> RelaxedR1CSSNARK let mut e = claim; let mut r: Vec = Vec::new(); - let mut cubic_polys: Vec> = Vec::new(); + let mut cubic_polys: Vec> = Vec::new(); let num_rounds = mem.size().log_2(); for _i in 0..num_rounds { let mut evals: Vec> = Vec::new(); @@ -967,10 +884,7 @@ impl> RelaxedR1CSSNARKTrait> RelaxedR1CSSNARKTrait> RelaxedR1CSSNARKTrait> RelaxedR1CSSNARKTrait G::Scalar { (0..M.len()) - .collect::>() - .par_iter() - .map(|&i| { + .into_par_iter() + .map(|i| { let (row, col, val) = M[i]; T_x[row] * T_y[col] * val }) - .reduce(|| G::Scalar::ZERO, |acc, x| acc + x) + .sum() }; let (T_x, T_y) = rayon::join( @@ -436,9 +432,8 @@ impl> RelaxedR1CSSNARKTrait>() - .par_iter() - .map(|&i| evaluate_with_table(M_vec[i], &T_x, &T_y)) + .into_par_iter() + .map(|i| evaluate_with_table(M_vec[i], &T_x, &T_y)) .collect() }; diff --git a/src/spartan/sumcheck.rs b/src/spartan/sumcheck.rs index 01d99c5f2..fc47a56bb 100644 --- a/src/spartan/sumcheck.rs +++ b/src/spartan/sumcheck.rs @@ -3,19 +3,18 @@ use super::polynomial::MultilinearPolynomial; use crate::errors::NovaError; use crate::traits::{Group, TranscriptEngineTrait, TranscriptReprTrait}; -use core::marker::PhantomData; -use ff::Field; +use ff::{Field, PrimeField}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "")] pub(crate) struct SumcheckProof { - compressed_polys: Vec>, + compressed_polys: Vec>, } impl SumcheckProof { - pub fn new(compressed_polys: Vec>) -> Self { + pub fn new(compressed_polys: Vec>) -> Self { Self { compressed_polys } } @@ -61,6 +60,34 @@ impl SumcheckProof { Ok((e, r)) } + #[inline] + pub(in crate::spartan) fn compute_eval_points_quadratic( + poly_A: &MultilinearPolynomial, + poly_B: &MultilinearPolynomial, + comb_func: &F, + ) -> (G::Scalar, G::Scalar) + where + F: Fn(&G::Scalar, &G::Scalar) -> G::Scalar + Sync, + { + let len = poly_A.len() / 2; + (0..len) + .into_par_iter() + .map(|i| { + // eval 0: bound_func is A(low) + let eval_point_0 = comb_func(&poly_A[i], &poly_B[i]); + + // eval 2: bound_func is -A(low) + 2*A(high) + let poly_A_bound_point = poly_A[len + i] + poly_A[len + i] - poly_A[i]; + let poly_B_bound_point = poly_B[len + i] + poly_B[len + i] - poly_B[i]; + let eval_point_2 = comb_func(&poly_A_bound_point, &poly_B_bound_point); + (eval_point_0, eval_point_2) + }) + .reduce( + || (G::Scalar::ZERO, G::Scalar::ZERO), + |a, b| (a.0 + b.0, a.1 + b.1), + ) + } + pub fn prove_quad( claim: &G::Scalar, num_rounds: usize, @@ -73,29 +100,12 @@ impl SumcheckProof { F: Fn(&G::Scalar, &G::Scalar) -> G::Scalar + Sync, { let mut r: Vec = Vec::new(); - let mut polys: Vec> = Vec::new(); + let mut polys: Vec> = Vec::new(); let mut claim_per_round = *claim; for _ in 0..num_rounds { let poly = { - let len = poly_A.len() / 2; - - // Make an iterator returning the contributions to the evaluations - let (eval_point_0, eval_point_2) = (0..len) - .into_par_iter() - .map(|i| { - // eval 0: bound_func is A(low) - let eval_point_0 = comb_func(&poly_A[i], &poly_B[i]); - - // eval 2: bound_func is -A(low) + 2*A(high) - let poly_A_bound_point = poly_A[len + i] + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B[len + i] + poly_B[len + i] - poly_B[i]; - let eval_point_2 = comb_func(&poly_A_bound_point, &poly_B_bound_point); - (eval_point_0, eval_point_2) - }) - .reduce( - || (G::Scalar::ZERO, G::Scalar::ZERO), - |a, b| (a.0 + b.0, a.1 + b.1), - ); + let (eval_point_0, eval_point_2) = + Self::compute_eval_points_quadratic(poly_A, poly_B, &comb_func); let evals = vec![eval_point_0, claim_per_round - eval_point_0, eval_point_2]; UniPoly::from_evals(&evals) @@ -136,30 +146,18 @@ impl SumcheckProof { transcript: &mut G::TE, ) -> Result<(Self, Vec, (Vec, Vec)), NovaError> where - F: Fn(&G::Scalar, &G::Scalar) -> G::Scalar, + F: Fn(&G::Scalar, &G::Scalar) -> G::Scalar + Sync, { let mut e = *claim; let mut r: Vec = Vec::new(); - let mut quad_polys: Vec> = Vec::new(); + let mut quad_polys: Vec> = Vec::new(); for _j in 0..num_rounds { let mut evals: Vec<(G::Scalar, G::Scalar)> = Vec::new(); for (poly_A, poly_B) in poly_A_vec.iter().zip(poly_B_vec.iter()) { - let mut eval_point_0 = G::Scalar::ZERO; - let mut eval_point_2 = G::Scalar::ZERO; - - let len = poly_A.len() / 2; - for i in 0..len { - // eval 0: bound_func is A(low) - eval_point_0 += comb_func(&poly_A[i], &poly_B[i]); - - // eval 2: bound_func is -A(low) + 2*A(high) - let poly_A_bound_point = poly_A[len + i] + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B[len + i] + poly_B[len + i] - poly_B[i]; - eval_point_2 += comb_func(&poly_A_bound_point, &poly_B_bound_point); - } - + let (eval_point_0, eval_point_2) = + Self::compute_eval_points_quadratic(poly_A, poly_B, &comb_func); evals.push((eval_point_0, eval_point_2)); } @@ -193,6 +191,55 @@ impl SumcheckProof { Ok((SumcheckProof::new(quad_polys), r, claims_prod)) } + #[inline] + pub(in crate::spartan) fn compute_eval_points_cubic( + poly_A: &MultilinearPolynomial, + poly_B: &MultilinearPolynomial, + poly_C: &MultilinearPolynomial, + poly_D: &MultilinearPolynomial, + comb_func: &F, + ) -> (G::Scalar, G::Scalar, G::Scalar) + where + F: Fn(&G::Scalar, &G::Scalar, &G::Scalar, &G::Scalar) -> G::Scalar + Sync, + { + let len = poly_A.len() / 2; + (0..len) + .into_par_iter() + .map(|i| { + // eval 0: bound_func is A(low) + let eval_point_0 = comb_func(&poly_A[i], &poly_B[i], &poly_C[i], &poly_D[i]); + + // eval 2: bound_func is -A(low) + 2*A(high) + let poly_A_bound_point = poly_A[len + i] + poly_A[len + i] - poly_A[i]; + let poly_B_bound_point = poly_B[len + i] + poly_B[len + i] - poly_B[i]; + let poly_C_bound_point = poly_C[len + i] + poly_C[len + i] - poly_C[i]; + let poly_D_bound_point = poly_D[len + i] + poly_D[len + i] - poly_D[i]; + let eval_point_2 = comb_func( + &poly_A_bound_point, + &poly_B_bound_point, + &poly_C_bound_point, + &poly_D_bound_point, + ); + + // eval 3: bound_func is -2A(low) + 3A(high); computed incrementally with bound_func applied to eval(2) + let poly_A_bound_point = poly_A_bound_point + poly_A[len + i] - poly_A[i]; + let poly_B_bound_point = poly_B_bound_point + poly_B[len + i] - poly_B[i]; + let poly_C_bound_point = poly_C_bound_point + poly_C[len + i] - poly_C[i]; + let poly_D_bound_point = poly_D_bound_point + poly_D[len + i] - poly_D[i]; + let eval_point_3 = comb_func( + &poly_A_bound_point, + &poly_B_bound_point, + &poly_C_bound_point, + &poly_D_bound_point, + ); + (eval_point_0, eval_point_2, eval_point_3) + }) + .reduce( + || (G::Scalar::ZERO, G::Scalar::ZERO, G::Scalar::ZERO), + |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2), + ) + } + pub fn prove_cubic_with_additive_term( claim: &G::Scalar, num_rounds: usize, @@ -207,49 +254,14 @@ impl SumcheckProof { F: Fn(&G::Scalar, &G::Scalar, &G::Scalar, &G::Scalar) -> G::Scalar + Sync, { let mut r: Vec = Vec::new(); - let mut polys: Vec> = Vec::new(); + let mut polys: Vec> = Vec::new(); let mut claim_per_round = *claim; for _ in 0..num_rounds { let poly = { - let len = poly_A.len() / 2; - // Make an iterator returning the contributions to the evaluations - let (eval_point_0, eval_point_2, eval_point_3) = (0..len) - .into_par_iter() - .map(|i| { - // eval 0: bound_func is A(low) - let eval_point_0 = comb_func(&poly_A[i], &poly_B[i], &poly_C[i], &poly_D[i]); - - // eval 2: bound_func is -A(low) + 2*A(high) - let poly_A_bound_point = poly_A[len + i] + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B[len + i] + poly_B[len + i] - poly_B[i]; - let poly_C_bound_point = poly_C[len + i] + poly_C[len + i] - poly_C[i]; - let poly_D_bound_point = poly_D[len + i] + poly_D[len + i] - poly_D[i]; - let eval_point_2 = comb_func( - &poly_A_bound_point, - &poly_B_bound_point, - &poly_C_bound_point, - &poly_D_bound_point, - ); - - // eval 3: bound_func is -2A(low) + 3A(high); computed incrementally with bound_func applied to eval(2) - let poly_A_bound_point = poly_A_bound_point + poly_A[len + i] - poly_A[i]; - let poly_B_bound_point = poly_B_bound_point + poly_B[len + i] - poly_B[i]; - let poly_C_bound_point = poly_C_bound_point + poly_C[len + i] - poly_C[i]; - let poly_D_bound_point = poly_D_bound_point + poly_D[len + i] - poly_D[i]; - let eval_point_3 = comb_func( - &poly_A_bound_point, - &poly_B_bound_point, - &poly_C_bound_point, - &poly_D_bound_point, - ); - (eval_point_0, eval_point_2, eval_point_3) - }) - .reduce( - || (G::Scalar::ZERO, G::Scalar::ZERO, G::Scalar::ZERO), - |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2), - ); + let (eval_point_0, eval_point_2, eval_point_3) = + Self::compute_eval_points_cubic(poly_A, poly_B, poly_C, poly_D, &comb_func); let evals = vec![ eval_point_0, @@ -291,25 +303,24 @@ impl SumcheckProof { // ax^2 + bx + c stored as vec![a,b,c] // ax^3 + bx^2 + cx + d stored as vec![a,b,c,d] #[derive(Debug)] -pub struct UniPoly { - coeffs: Vec, +pub struct UniPoly { + coeffs: Vec, } // ax^2 + bx + c stored as vec![a,c] // ax^3 + bx^2 + cx + d stored as vec![a,c,d] #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CompressedUniPoly { - coeffs_except_linear_term: Vec, - _p: PhantomData, +pub struct CompressedUniPoly { + coeffs_except_linear_term: Vec, } -impl UniPoly { - pub fn from_evals(evals: &[G::Scalar]) -> Self { +impl UniPoly { + pub fn from_evals(evals: &[Scalar]) -> Self { // we only support degree-2 or degree-3 univariate polynomials assert!(evals.len() == 3 || evals.len() == 4); let coeffs = if evals.len() == 3 { // ax^2 + bx + c - let two_inv = G::Scalar::from(2).invert().unwrap(); + let two_inv = Scalar::from(2).invert().unwrap(); let c = evals[0]; let a = two_inv * (evals[2] - evals[1] - evals[1] + c); @@ -317,8 +328,8 @@ impl UniPoly { vec![c, b, a] } else { // ax^3 + bx^2 + cx + d - let two_inv = G::Scalar::from(2).invert().unwrap(); - let six_inv = G::Scalar::from(6).invert().unwrap(); + let two_inv = Scalar::from(2).invert().unwrap(); + let six_inv = Scalar::from(6).invert().unwrap(); let d = evals[0]; let a = six_inv @@ -341,18 +352,18 @@ impl UniPoly { self.coeffs.len() - 1 } - pub fn eval_at_zero(&self) -> G::Scalar { + pub fn eval_at_zero(&self) -> Scalar { self.coeffs[0] } - pub fn eval_at_one(&self) -> G::Scalar { + pub fn eval_at_one(&self) -> Scalar { (0..self.coeffs.len()) .into_par_iter() .map(|i| self.coeffs[i]) - .reduce(|| G::Scalar::ZERO, |a, b| a + b) + .sum() } - pub fn evaluate(&self, r: &G::Scalar) -> G::Scalar { + pub fn evaluate(&self, r: &Scalar) -> Scalar { let mut eval = self.coeffs[0]; let mut power = *r; for coeff in self.coeffs.iter().skip(1) { @@ -362,27 +373,26 @@ impl UniPoly { eval } - pub fn compress(&self) -> CompressedUniPoly { + pub fn compress(&self) -> CompressedUniPoly { let coeffs_except_linear_term = [&self.coeffs[0..1], &self.coeffs[2..]].concat(); assert_eq!(coeffs_except_linear_term.len() + 1, self.coeffs.len()); CompressedUniPoly { coeffs_except_linear_term, - _p: Default::default(), } } } -impl CompressedUniPoly { +impl CompressedUniPoly { // we require eval(0) + eval(1) = hint, so we can solve for the linear term as: // linear_term = hint - 2 * constant_term - deg2 term - deg3 term - pub fn decompress(&self, hint: &G::Scalar) -> UniPoly { + pub fn decompress(&self, hint: &Scalar) -> UniPoly { let mut linear_term = *hint - self.coeffs_except_linear_term[0] - self.coeffs_except_linear_term[0]; for i in 1..self.coeffs_except_linear_term.len() { linear_term -= self.coeffs_except_linear_term[i]; } - let mut coeffs: Vec = Vec::new(); + let mut coeffs: Vec = Vec::new(); coeffs.push(self.coeffs_except_linear_term[0]); coeffs.push(linear_term); coeffs.extend(&self.coeffs_except_linear_term[1..]); @@ -391,7 +401,7 @@ impl CompressedUniPoly { } } -impl TranscriptReprTrait for UniPoly { +impl TranscriptReprTrait for UniPoly { fn to_transcript_bytes(&self) -> Vec { let coeffs = self.compress().coeffs_except_linear_term; coeffs.as_slice().to_transcript_bytes() diff --git a/src/traits/commitment.rs b/src/traits/commitment.rs index 9b4725fc3..4ac8349ce 100644 --- a/src/traits/commitment.rs +++ b/src/traits/commitment.rs @@ -6,10 +6,12 @@ use crate::{ }; use core::{ fmt::Debug, - ops::{Add, AddAssign, Mul, MulAssign}, + ops::{Add, AddAssign}, }; use serde::{Deserialize, Serialize}; +use super::ScalarMul; + /// Defines basic operations on commitments pub trait CommitmentOps: Add + AddAssign @@ -31,12 +33,6 @@ impl CommitmentOpsOwned for T where { } -/// A helper trait for types implementing a multiplication of a commitment with a scalar -pub trait ScalarMul: Mul + MulAssign {} - -impl ScalarMul for T where T: Mul + MulAssign -{} - /// This trait defines the behavior of the commitment pub trait CommitmentTrait: Clone diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 5138cea8d..91d8d320e 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -41,8 +41,7 @@ pub trait Group: + for<'de> Deserialize<'de>; /// A type representing an element of the scalar field of the group - type Scalar: PrimeField - + PrimeFieldBits + type Scalar: PrimeFieldBits + PrimeFieldExt + Send + Sync @@ -236,11 +235,9 @@ pub trait PrimeFieldExt: PrimeField { impl> TranscriptReprTrait for &[T] { fn to_transcript_bytes(&self) -> Vec { - (0..self.len()) - .map(|i| self[i].to_transcript_bytes()) - .collect::>() - .into_iter() - .flatten() + self + .iter() + .flat_map(|t| t.to_transcript_bytes()) .collect::>() } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 000000000..51e02ccd5 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,449 @@ +//! Basic utils +#![allow(unused)] +use std::sync::Arc; + +use crate::errors::NovaError; +use crate::spartan::polynomial::MultilinearPolynomial; +use crate::traits::Group; +use ff::{Field, PrimeField}; +use itertools::Itertools; +use rand_core::RngCore; +use rayon::prelude::{IntoParallelRefMutIterator, ParallelIterator}; +use serde::{Deserialize, Serialize}; + +/// A matrix structure represented on a sparse form. +/// First element is row index, second column, third value stored +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SparseMatrix { + n_rows: usize, + n_cols: usize, + coeffs: Vec<(usize, usize, F)>, +} + +impl SparseMatrix { + pub fn new(n_rows: usize, n_cols: usize) -> Self { + Self { + n_rows, + n_cols, + coeffs: vec![], + } + } + + pub fn with_coeffs(n_rows: usize, n_cols: usize, coeffs: Vec<(usize, usize, F)>) -> Self { + Self { + n_rows, + n_cols, + coeffs, + } + } + + // Return the number of rows of this matrix. + pub fn n_rows(&self) -> usize { + self.n_rows + } + + // Returns a mutable reference to the number of rows of this matrix. + pub fn n_rows_mut(&mut self) -> &mut usize { + &mut self.n_rows + } + + // Return the number of cols of this matrix. + pub fn n_cols(&self) -> usize { + self.n_cols + } + + // Returns a mutable reference to the number of cols of this matrix. + pub fn n_cols_mut(&mut self) -> &mut usize { + &mut self.n_cols + } + + // Return the non-0 coefficients of this matrix. + pub fn coeffs(&self) -> &[(usize, usize, F)] { + self.coeffs.as_slice() + } + + pub(crate) fn is_valid( + &self, + num_cons: usize, + num_vars: usize, + num_io: usize, + ) -> Result<(), NovaError> { + if self.n_rows >= num_cons || self.n_cols > num_io + num_vars { + Err(NovaError::InvalidIndex) + } else { + Ok(()) + } + } + + /// Pad matrix so that its columns and rows are powers of two + pub(crate) fn pad(&mut self) { + // Find the desired dimensions after padding + let rows = self.n_rows(); + let cols = self.n_cols(); + + // Since we padd with 0's and our matrix repr is sparse, we just need + // to update the rows and cols attrs of the matrix. + *self.n_rows_mut() = rows.next_power_of_two(); + *self.n_cols_mut() = cols.next_power_of_two(); + } + + // Gives the MLE of the given matrix. + pub fn to_mle(&self) -> MultilinearPolynomial { + // Matrices might need to get padded before turned into an MLE + let mut padded_matrix = self.clone(); + padded_matrix.pad(); + + sparse_vec_to_mle::( + padded_matrix.n_rows(), + padded_matrix.n_cols(), + padded_matrix.coeffs().to_vec(), + ) + } +} + +// NOTE: the method is called "sparse_vec_to_mle", but inputs are n_rows & n_cols, and a normal +// vector does not have rows&cols. This is because in this case the vec comes from matrix +// coefficients, maybe the method should be renamed, because is not to convert 'any' vector but a +// vector of matrix coefficients. A better option probably is to replace the two inputs n_rows & +// n_cols by directly the n_vars. +pub fn sparse_vec_to_mle( + n_rows: usize, + n_cols: usize, + v: Vec<(usize, usize, F)>, +) -> MultilinearPolynomial { + let n_vars: usize = (log2(n_rows) + log2(n_cols)) as usize; // n_vars = s + s' + let mut padded_vec = vec![F::ZERO; 1 << n_vars]; + v.iter().copied().for_each(|(row, col, coeff)| { + padded_vec[(n_cols * row) + col] = coeff; + }); + + dense_vec_to_mle(n_vars, &padded_vec) +} + +pub fn dense_vec_to_mle(n_vars: usize, v: &[F]) -> MultilinearPolynomial { + // Pad to 2^n_vars + let v_padded: Vec = [ + v, + std::iter::repeat(F::ZERO) + .take((1 << n_vars) - v.len()) + .collect_vec() + .as_slice(), + ] + .concat(); + MultilinearPolynomial::new(v_padded) +} + +pub fn vector_add(a: &Vec, b: &Vec) -> Vec { + assert_eq!(a.len(), b.len(), "Vector addition with different lengths"); + let mut res = Vec::with_capacity(a.len()); + for i in 0..a.len() { + res.push(a[i] + b[i]); + } + + res +} + +pub fn vector_elem_product(a: &Vec, e: F) -> Vec { + let mut res = Vec::with_capacity(a.len()); + for &item in a { + res.push(item * e); + } + + res +} + +// XXX: This could be implemented via Mul trait in the lib. We should consider as it will reduce imports. +#[allow(dead_code)] +pub fn matrix_vector_product(matrix: &Vec>, vector: &Vec) -> Vec { + assert_ne!(matrix.len(), 0, "empty-row matrix"); + assert_ne!(matrix[0].len(), 0, "empty-col matrix"); + assert_eq!( + matrix[0].len(), + vector.len(), + "matrix rows != vector length" + ); + let mut res = Vec::with_capacity(matrix.len()); + for row in matrix { + let mut sum = F::ZERO; + for j in 0..row.len() { + sum += row[j] * vector[j]; + } + res.push(sum); + } + + res +} + +// Matrix vector product where matrix is sparse +// First element is row index, second column, third value stored +// XXX: This could be implemented via Mul trait in the lib. We should consider as it will reduce imports. +pub fn matrix_vector_product_sparse( + matrix: &SparseMatrix, + vector: &[F], +) -> Vec { + let mut res = vec![F::ZERO; matrix.n_rows()]; + for &(row, col, value) in matrix.coeffs.iter() { + res[row] += value * vector[col]; + } + res +} + +pub fn hadamard_product(a: &Vec, b: &Vec) -> Vec { + assert_eq!(a.len(), b.len(), "Hadamard needs same len vectors"); + let mut res = Vec::with_capacity(a.len()); + for i in 0..a.len() { + res.push(a[i] * b[i]); + } + + res +} + +/// Sample a random list of multilinear polynomials. +/// Returns +/// - the list of polynomials, +/// - its sum of polynomial evaluations over the boolean hypercube. +pub fn random_mle_list( + nv: usize, + degree: usize, + mut rng: &mut R, +) -> (Vec>>, F) { + let mut multiplicands = Vec::with_capacity(degree); + for _ in 0..degree { + multiplicands.push(Vec::with_capacity(1 << nv)) + } + let mut sum = F::ZERO; + + for _ in 0..(1 << nv) { + let mut product = F::ONE; + + for e in multiplicands.iter_mut() { + let val = F::random(&mut rng); + e.push(val); + product *= val; + } + sum += product; + } + + let list = multiplicands + .into_iter() + .map(|x| Arc::new(MultilinearPolynomial::new(x))) + .collect(); + + (list, sum) +} + +// Build a randomize list of mle-s whose sum is zero. +pub fn random_zero_mle_list( + nv: usize, + degree: usize, + mut rng: &mut R, +) -> Vec>> { + let mut multiplicands = Vec::with_capacity(degree); + for _ in 0..degree { + multiplicands.push(Vec::with_capacity(1 << nv)) + } + for _ in 0..(1 << nv) { + multiplicands[0].push(F::ZERO); + for e in multiplicands.iter_mut().skip(1) { + e.push(F::random(&mut rng)); + } + } + + multiplicands + .into_iter() + .map(|x| Arc::new(MultilinearPolynomial::new(x))) + .collect() +} + +pub(crate) fn log2(x: usize) -> u32 { + if x == 0 { + 0 + } else if x.is_power_of_two() { + 1usize.leading_zeros() - x.leading_zeros() + } else { + 0usize.leading_zeros() - x.leading_zeros() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hypercube::BooleanHypercube; + + use pasta_curves::Fq; + + fn to_F_vec(v: Vec) -> Vec { + v.iter().map(|x| F::from(*x)).collect() + } + + fn to_F_matrix(m: Vec>) -> Vec> { + m.iter().map(|x| to_F_vec(x.clone())).collect() + } + + fn test_vector_add_with() { + let a = to_F_vec::(vec![1, 2, 3]); + let b = to_F_vec::(vec![4, 5, 6]); + let res = vector_add::(&a, &b); + assert_eq!(res, to_F_vec::(vec![5, 7, 9])); + } + + fn test_vector_elem_product_with() { + let a = to_F_vec::(vec![1, 2, 3]); + let e = F::from(2); + let res = vector_elem_product(&a, e); + assert_eq!(res, to_F_vec::(vec![2, 4, 6])); + } + + fn test_matrix_vector_product_with() { + let matrix = vec![vec![1, 2, 3], vec![4, 5, 6]]; + let vector = vec![1, 2, 3]; + let A = to_F_matrix::(matrix); + let z = to_F_vec::(vector); + let res = matrix_vector_product(&A, &z); + + assert_eq!(res, to_F_vec::(vec![14, 32])); + } + + fn test_hadamard_product_with() { + let a = to_F_vec::(vec![1, 2, 3]); + let b = to_F_vec::(vec![4, 5, 6]); + let res = hadamard_product(&a, &b); + assert_eq!(res, to_F_vec::(vec![4, 10, 18])); + } + + fn test_matrix_vector_product_sparse_with() { + let matrix = vec![ + (0, 0, F::from(1u64)), + (0, 1, F::from(2u64)), + (0, 2, F::from(3u64)), + (1, 0, F::from(4u64)), + (1, 1, F::from(5u64)), + (1, 2, F::from(6u64)), + ]; + + let z = to_F_vec::(vec![1, 2, 3]); + let res = matrix_vector_product_sparse::(&SparseMatrix::::with_coeffs(2, 3, matrix), &z); + + assert_eq!(res, to_F_vec::(vec![14, 32])); + } + + fn test_sparse_matrix_n_cols_rows_with() { + let matrix = vec![ + (0, 0, F::from(1u64)), + (0, 1, F::from(2u64)), + (0, 2, F::from(3u64)), + (1, 0, F::from(4u64)), + (1, 1, F::from(5u64)), + (1, 2, F::from(6u64)), + (4, 5, F::from(1u64)), + ]; + let A = SparseMatrix::::with_coeffs(5, 6, matrix.clone()); + assert_eq!(A.n_cols(), 6); + assert_eq!(A.n_rows(), 5); + + // Since is sparse, the empty rows/cols at the end are not accounted unless we provide the info. + let A = SparseMatrix::::with_coeffs(10, 10, matrix); + assert_eq!(A.n_cols(), 10); + assert_eq!(A.n_rows(), 10); + } + + fn test_matrix_to_mle_with() { + let A = SparseMatrix::::with_coeffs( + 5, + 5, + vec![ + (0usize, 0usize, F::from(1u64)), + (0, 1, F::from(2u64)), + (0, 2, F::from(3u64)), + (1, 0, F::from(4u64)), + (1, 1, F::from(5u64)), + (1, 2, F::from(6u64)), + (3, 4, F::from(1u64)), + ], + ); + + let A_mle = A.to_mle(); + assert_eq!(A_mle.len(), 64); // 5x5 matrix, thus 3bit x 3bit, thus 2^6=64 evals + + // hardcoded testvector to ensure that in the future the SparseMatrix.to_mle method holds + let expected = vec![ + F::from(1u64), + F::from(2u64), + F::from(3u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(4u64), + F::from(5u64), + F::from(6u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(0u64), + F::from(1u64), + // the rest are zeroes + ]; + assert_eq!(A_mle.Z[..29], expected); + assert_eq!(A_mle.Z[29..], vec![F::ZERO; 64 - 29]); + + // check that the A_mle evaluated over the boolean hypercube equals the matrix A_i_j values + let bhc = BooleanHypercube::::new(A_mle.get_num_vars()); + let mut A_padded = A; + A_padded.pad(); + for term in A_padded.coeffs.iter() { + let (i, j, coeff) = term; + let s_i_j = bhc.evaluate_at(i * A_padded.n_cols + j); + assert_eq!(&A_mle.evaluate(&s_i_j), coeff) + } + } + + #[test] + fn test_vector_add() { + test_vector_add_with::(); + } + + #[test] + fn test_vector_elem_product() { + test_vector_elem_product_with::(); + } + + #[test] + fn test_matrix_vector_product() { + test_matrix_vector_product_with::(); + } + + #[test] + fn test_hadamard_product() { + test_hadamard_product_with::(); + } + + #[test] + fn test_matrix_vector_product_sparse() { + test_matrix_vector_product_sparse_with::(); + } + + #[test] + fn test_sparse_matrix_n_cols_rows() { + test_sparse_matrix_n_cols_rows_with::(); + } + + #[test] + fn test_matrix_to_mle() { + test_matrix_to_mle_with::(); + } +} diff --git a/tests/nimfs.rs b/tests/nimfs.rs new file mode 100644 index 000000000..d3deaa4ef --- /dev/null +++ b/tests/nimfs.rs @@ -0,0 +1,110 @@ +#![cfg(feature = "hypernova")] +use std::marker::PhantomData; + +use bellperson::{gadgets::num::AllocatedNum, ConstraintSystem, SynthesisError}; +use ff::{Field, PrimeField}; +use nova_snark::{ + ccs::{CCS, NIMFS}, + traits::{circuit::StepCircuit, Group}, + NovaShape, ShapeCS, +}; +use pasta_curves::Ep; + +#[derive(Clone, Debug, Default)] +struct CubicCircuit { + _p: PhantomData, +} + +impl StepCircuit for CubicCircuit +where + F: PrimeField, +{ + fn arity(&self) -> usize { + 1 + } + + fn synthesize>( + &self, + cs: &mut CS, + z: &[AllocatedNum], + ) -> Result>, SynthesisError> { + // Consider a cubic equation: `x^3 + x + 5 = y`, where `x` and `y` are respectively the input and output. + let x = &z[0]; + let x_sq = x.square(cs.namespace(|| "x_sq"))?; + let x_cu = x_sq.mul(cs.namespace(|| "x_cu"), x)?; + let y = &z[1]; + + cs.enforce( + || "y = x^3 + x + 5", + |lc| { + lc + x_cu.get_variable() + + x.get_variable() + + CS::one() + + CS::one() + + CS::one() + + CS::one() + + CS::one() + }, + |lc| lc + CS::one(), + |lc| lc + y.get_variable(), + ); + + Ok(vec![y.clone()]) + } + + fn output(&self, z: &[F]) -> Vec { + vec![z[0] * z[0] * z[0] + z[0] + F::from(5u64)] + } +} + +#[test] +fn integration_folding() { + integration_folding_test::() +} + +fn integration_folding_test() { + let circuit = CubicCircuit::::default(); + let mut cs: ShapeCS = ShapeCS::new(); + // Generate the inputs: + // Here we need both the R1CSShape so that we can generate the CCS -> NIMFS and also the witness values. + let three = AllocatedNum::alloc(&mut cs, || Ok(G::Scalar::from(3u64))).unwrap(); + let thirty_five = AllocatedNum::alloc(&mut cs, || Ok(G::Scalar::from(35u64))).unwrap(); + let _ = circuit.synthesize(&mut cs, &[three, thirty_five]); + let (r1cs_shape, _) = cs.r1cs_shape(); + + let ccs = CCS::::from_r1cs(r1cs_shape); + + // Generate NIMFS object. + let mut nimfs = NIMFS::init( + ccs, + // Note we constructed z on the fly with the previously-used witness. + vec![ + G::Scalar::ONE, + G::Scalar::from(3u64), + G::Scalar::from(35u64), + ], + b"test_nimfs", + ); + + let mut nimfs_witness = vec![G::Scalar::from(3u64), G::Scalar::from(35u64)]; + + // Now, the NIMFS should satisfy correctly as we have inputed valid starting inpuits for the first LCCCS contained instance: + assert!(nimfs.is_sat(&nimfs_witness).is_ok()); + + // Now let's create a valid CCCS instance and fold it: + let valid_cccs = nimfs.new_cccs(vec![ + G::Scalar::ONE, + G::Scalar::from(2u64), + G::Scalar::from(15u64), + ]); + let cccs_witness = vec![G::Scalar::from(2u64), G::Scalar::from(15u64)]; + + let (r_x_prime, rho) = nimfs.prepare_folding(); + let (sigmas, thetas) = + nimfs.compute_sigmas_and_thetas(&cccs_witness, &valid_cccs, &nimfs_witness, &r_x_prime); + nimfs.fold(&valid_cccs, sigmas, thetas, r_x_prime, rho); + nimfs.fold_witness(&valid_cccs, &cccs_witness, &mut nimfs_witness, rho); + + // Since the instance was correct, the NIMFS should still be satisfied. + assert!(nimfs.is_sat(&nimfs_witness).is_ok()); +}