From 8256c27609a2effbf9a255f0709eb0b1c3055b36 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Mon, 11 Sep 2023 09:31:23 +0200 Subject: [PATCH] Port/hypernova multifolding (#10) * Port HyperNova's multifolding from https://github.com/privacy-scaling-explorations/multifolding-poc adapting and refactoring some of its methods and structs. Note: adapted mle.rs methods from dense to sparse repr. Co-authored-by: George Kadianakis * HyperNova: move CCS struct outside of LCCCS & CCCS HyperNova nimfs: move CCS structure outside of LCCCS & CCCS, to avoid carrying around the whole CCS and duplicating data when is not needed. Also add feature flags for the folding schemes. --------- Co-authored-by: George Kadianakis --- Cargo.toml | 4 +- src/ccs/mod.rs | 8 +- src/ccs/r1cs.rs | 12 +- src/folding/circuits/cyclefold.rs | 2 +- src/folding/hypernova/cccs.rs | 226 ++++++++ src/folding/hypernova/lcccs.rs | 181 +++++++ src/folding/hypernova/mod.rs | 5 + src/folding/hypernova/nimfs.rs | 657 +++++++++++++++++++++++ src/folding/hypernova/utils.rs | 360 +++++++++++++ src/folding/mod.rs | 3 + src/folding/nova/mod.rs | 1 + src/folding/nova/nifs.rs | 2 + src/pedersen.rs | 13 +- src/utils/espresso/sum_check/verifier.rs | 2 +- src/utils/espresso/virtual_polynomial.rs | 2 +- src/utils/hypercube.rs | 77 +++ src/utils/mle.rs | 167 ++++++ src/utils/mod.rs | 2 + src/utils/vec.rs | 14 +- 19 files changed, 1711 insertions(+), 27 deletions(-) create mode 100644 src/folding/hypernova/cccs.rs create mode 100644 src/folding/hypernova/lcccs.rs create mode 100644 src/folding/hypernova/mod.rs create mode 100644 src/folding/hypernova/nimfs.rs create mode 100644 src/folding/hypernova/utils.rs create mode 100644 src/utils/hypercube.rs create mode 100644 src/utils/mle.rs diff --git a/Cargo.toml b/Cargo.toml index c0a732f..a8f99d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,9 @@ ark-vesta = {version="0.4.0"} ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = ["crh"] } [features] -default = ["parallel"] +default = ["parallel", "nova", "hypernova"] +hypernova=[] +nova=[] parallel = [ "ark-std/parallel", diff --git a/src/ccs/mod.rs b/src/ccs/mod.rs index 49163fd..23ba24b 100644 --- a/src/ccs/mod.rs +++ b/src/ccs/mod.rs @@ -102,15 +102,19 @@ impl CCS { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; - use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z}; + use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z as r1cs_get_test_z}; + use ark_ff::PrimeField; use ark_pallas::Projective; pub fn get_test_ccs() -> CCS { let r1cs = get_test_r1cs::(); CCS::::from_r1cs(r1cs) } + pub fn get_test_z(input: usize) -> Vec { + r1cs_get_test_z(input) + } /// Test that a basic CCS relation can be satisfied #[test] diff --git a/src/ccs/r1cs.rs b/src/ccs/r1cs.rs index c5f1c80..26084ad 100644 --- a/src/ccs/r1cs.rs +++ b/src/ccs/r1cs.rs @@ -75,24 +75,24 @@ pub mod tests { pub fn get_test_r1cs() -> R1CS { // R1CS for: x^3 + x + 5 = y (example from article // https://www.vitalik.ca/general/2016/12/10/qap.html ) - let A = dense_matrix_to_sparse(to_F_matrix::(vec![ + let A = to_F_matrix::(vec![ vec![0, 1, 0, 0, 0, 0], vec![0, 0, 0, 1, 0, 0], vec![0, 1, 0, 0, 1, 0], vec![5, 0, 0, 0, 0, 1], - ])); - let B = dense_matrix_to_sparse(to_F_matrix::(vec![ + ]); + let B = to_F_matrix::(vec![ vec![0, 1, 0, 0, 0, 0], vec![0, 1, 0, 0, 0, 0], vec![1, 0, 0, 0, 0, 0], vec![1, 0, 0, 0, 0, 0], - ])); - let C = dense_matrix_to_sparse(to_F_matrix::(vec![ + ]); + let C = to_F_matrix::(vec![ vec![0, 0, 0, 1, 0, 0], vec![0, 0, 0, 0, 1, 0], vec![0, 0, 0, 0, 0, 1], vec![0, 0, 1, 0, 0, 0], - ])); + ]); R1CS:: { l: 1, A, B, C } } diff --git a/src/folding/circuits/cyclefold.rs b/src/folding/circuits/cyclefold.rs index 5d82770..9b3c708 100644 --- a/src/folding/circuits/cyclefold.rs +++ b/src/folding/circuits/cyclefold.rs @@ -28,7 +28,7 @@ impl>> ECRLC { } #[cfg(test)] -mod test { +mod tests { use super::*; use ark_ff::{BigInteger, PrimeField}; use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; diff --git a/src/folding/hypernova/cccs.rs b/src/folding/hypernova/cccs.rs new file mode 100644 index 0000000..9922dbf --- /dev/null +++ b/src/folding/hypernova/cccs.rs @@ -0,0 +1,226 @@ +use ark_ec::CurveGroup; +use ark_ff::PrimeField; +use ark_std::One; +use ark_std::Zero; +use std::ops::Add; +use std::sync::Arc; + +use ark_std::{rand::Rng, UniformRand}; + +use super::utils::compute_sum_Mz; +use crate::ccs::CCS; +use crate::pedersen::{Params as PedersenParams, Pedersen}; +use crate::utils::hypercube::BooleanHypercube; +use crate::utils::mle::matrix_to_mle; +use crate::utils::mle::vec_to_mle; +use crate::utils::virtual_polynomial::VirtualPolynomial; +use crate::Error; + +/// Witness for the LCCCS & CCCS, containing the w vector, and the r_w used as randomness in the Pedersen commitment. +#[derive(Debug, Clone)] +pub struct Witness { + pub w: Vec, + pub r_w: F, // randomness used in the Pedersen commitment of w +} + +/// Committed CCS instance +#[derive(Debug, Clone)] +pub struct CCCS { + // Commitment to witness + pub C: C, + // Public input/output + pub x: Vec, +} + +impl CCS { + pub fn to_cccs( + &self, + rng: &mut R, + pedersen_params: &PedersenParams, + z: &[C::ScalarField], + ) -> (CCCS, Witness) { + let w: Vec = z[(1 + self.l)..].to_vec(); + let r_w = C::ScalarField::rand(rng); + let C = Pedersen::::commit(pedersen_params, &w, &r_w); + + ( + CCCS:: { + C, + x: z[1..(1 + self.l)].to_vec(), + }, + Witness:: { w, r_w }, + ) + } + + /// 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 fn compute_q(&self, z: &Vec) -> VirtualPolynomial { + let z_mle = vec_to_mle(self.s_prime, z); + let mut q = VirtualPolynomial::::new(self.s); + + for i in 0..self.q { + let mut prod: VirtualPolynomial = + VirtualPolynomial::::new(self.s); + for j in self.S[i].clone() { + let M_j = matrix_to_mle(self.M[j].clone()); + + let sum_Mz = compute_sum_Mz(M_j, &z_mle, self.s_prime); + + // 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)], C::ScalarField::one()) + .unwrap(); + } else { + prod.mul_by_mle(Arc::new(sum_Mz), C::ScalarField::one()) + .unwrap(); + } + } + // Multiply by the product by the coefficient c_i + prod.scalar_mul(&self.c[i]); + // Add it to the running sum + q = q.add(&prod); + } + q + } + + /// 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, + z: &Vec, + beta: &[C::ScalarField], + ) -> VirtualPolynomial { + let q = self.compute_q(z); + q.build_f_hat(beta).unwrap() + } +} + +impl CCCS { + /// Perform the check of the CCCS instance described at section 4.1 + pub fn check_relation( + &self, + pedersen_params: &PedersenParams, + ccs: &CCS, + w: &Witness, + ) -> Result<(), Error> { + // 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.C, Pedersen::commit(pedersen_params, &w.w, &w.r_w)); + + // check CCCS relation + let z: Vec = + [vec![C::ScalarField::one()], self.x.clone(), w.w.to_vec()].concat(); + + // A CCCS relation is satisfied if the q(x) multivariate polynomial evaluates to zero in the hypercube + let q_x = ccs.compute_q(&z); + for x in BooleanHypercube::new(ccs.s) { + if !q_x.evaluate(&x).unwrap().is_zero() { + return Err(Error::NotSatisfied); + } + } + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::ccs::tests::{get_test_ccs, get_test_z}; + use ark_std::test_rng; + use ark_std::UniformRand; + + use ark_pallas::{Fr, Projective}; + + /// 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() { + let mut rng = test_rng(); + + let ccs = get_test_ccs::(); + let z = get_test_z(3); + + let q = ccs.compute_q(&z); + + // Evaluate inside the hypercube + for x in BooleanHypercube::new(ccs.s) { + assert_eq!(Fr::zero(), q.evaluate(&x).unwrap()); + } + + // Evaluate outside the hypercube + let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + assert_ne!(Fr::zero(), q.evaluate(&beta).unwrap()); + } + + /// Perform some sanity checks on Q(x). + #[test] + fn test_compute_Q() { + let mut rng = test_rng(); + + let ccs: CCS = get_test_ccs(); + let z = get_test_z(3); + ccs.check_relation(&z).unwrap(); + + let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + + // Compute Q(x) = eq(beta, x) * q(x). + let Q = ccs.compute_Q(&z, &beta); + + // 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(Fr::zero(), |acc, result| acc + result); + assert_eq!(r, Fr::zero()); + } + + /// 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() { + let mut rng = test_rng(); + + let ccs: CCS = get_test_ccs(); + let z = get_test_z(3); + ccs.check_relation(&z).unwrap(); + + // 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 = ccs.compute_q(&z); + for d in BooleanHypercube::new(ccs.s) { + let Q_at_d = ccs.compute_Q(&z, &d); + + // 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(Fr::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(|_| Fr::rand(&mut rng)).collect(); + let Q_at_r = ccs.compute_Q(&z, &r); + + // 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(Fr::zero(), |acc, result| acc + result); + assert_ne!(G_at_r, q.evaluate(&r).unwrap()); + } +} diff --git a/src/folding/hypernova/lcccs.rs b/src/folding/hypernova/lcccs.rs new file mode 100644 index 0000000..be4a34c --- /dev/null +++ b/src/folding/hypernova/lcccs.rs @@ -0,0 +1,181 @@ +use ark_ec::CurveGroup; +use ark_poly::DenseMultilinearExtension; +use ark_std::One; +use std::sync::Arc; + +use ark_std::{rand::Rng, UniformRand}; + +use super::cccs::Witness; +use super::utils::{compute_all_sum_Mz_evals, compute_sum_Mz}; +use crate::ccs::CCS; +use crate::pedersen::{Params as PedersenParams, Pedersen}; +use crate::utils::mle::{matrix_to_mle, vec_to_mle}; +use crate::utils::virtual_polynomial::VirtualPolynomial; +use crate::Error; + +/// Linearized Committed CCS instance +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct LCCCS { + // Commitment to witness + pub C: C, + // Relaxation factor of z for folded LCCCS + pub u: C::ScalarField, + // Public input/output + pub x: Vec, + // Random evaluation point for the v_i + pub r_x: Vec, + // Vector of v_i + pub v: Vec, +} + +impl CCS { + /// 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: &[C::ScalarField], r: &[C::ScalarField]) -> Vec { + compute_all_sum_Mz_evals(&self.M, &z.to_vec(), r, self.s_prime) + } + + pub fn to_lcccs( + &self, + rng: &mut R, + pedersen_params: &PedersenParams, + z: &[C::ScalarField], + ) -> (LCCCS, Witness) { + let w: Vec = z[(1 + self.l)..].to_vec(); + let r_w = C::ScalarField::rand(rng); + let C = Pedersen::commit(pedersen_params, &w, &r_w); + + let r_x: Vec = (0..self.s).map(|_| C::ScalarField::rand(rng)).collect(); + let v = self.compute_v_j(z, &r_x); + + ( + LCCCS:: { + C, + u: C::ScalarField::one(), + x: z[1..(1 + self.l)].to_vec(), + r_x, + v, + }, + Witness:: { w, r_w }, + ) + } +} + +impl LCCCS { + /// Compute all L_j(x) polynomials + pub fn compute_Ls( + &self, + ccs: &CCS, + z: &Vec, + ) -> Vec> { + let z_mle = vec_to_mle(ccs.s_prime, z); + // Convert all matrices to MLE + let M_x_y_mle: Vec> = + ccs.M.clone().into_iter().map(matrix_to_mle).collect(); + + let mut vec_L_j_x = Vec::with_capacity(ccs.t); + for M_j in M_x_y_mle { + let sum_Mz = compute_sum_Mz(M_j, &z_mle, ccs.s_prime); + let sum_Mz_virtual = + VirtualPolynomial::new_from_mle(&Arc::new(sum_Mz.clone()), C::ScalarField::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 + } + + /// Perform the check of the LCCCS instance described at section 4.2 + pub fn check_relation( + &self, + pedersen_params: &PedersenParams, + ccs: &CCS, + w: &Witness, + ) -> Result<(), Error> { + // 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.C, Pedersen::commit(pedersen_params, &w.w, &w.r_w)); + + // check CCS relation + let z: Vec = [vec![self.u], self.x.clone(), w.w.to_vec()].concat(); + let computed_v = compute_all_sum_Mz_evals(&ccs.M, &z, &self.r_x, ccs.s_prime); + assert_eq!(computed_v, self.v); + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use ark_std::Zero; + + use crate::ccs::tests::{get_test_ccs, get_test_z}; + use crate::utils::hypercube::BooleanHypercube; + use ark_std::test_rng; + + use ark_pallas::{Fr, Projective}; + + #[test] + /// Test linearized CCCS v_j against the L_j(x) + fn test_lcccs_v_j() { + let mut rng = test_rng(); + + let ccs = get_test_ccs(); + let z = get_test_z(3); + ccs.check_relation(&z.clone()).unwrap(); + + let pedersen_params = Pedersen::::new_params(&mut rng, ccs.n - ccs.l - 1); + let (lcccs, _) = ccs.to_lcccs(&mut rng, &pedersen_params, &z); + // with our test vector comming from R1CS, v should have length 3 + assert_eq!(lcccs.v.len(), 3); + + let vec_L_j_x = lcccs.compute_Ls(&ccs, &z); + 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(Fr::zero(), |acc, result| acc + result); + assert_eq!(v_i, sum_L_j_x); + } + } + + /// Given a bad z, check that the v_j should not match with the L_j(x) + #[test] + fn test_bad_v_j() { + let mut rng = test_rng(); + + let ccs = get_test_ccs(); + let z = get_test_z(3); + ccs.check_relation(&z.clone()).unwrap(); + + // Mutate z so that the relation does not hold + let mut bad_z = z.clone(); + bad_z[3] = Fr::zero(); + assert!(ccs.check_relation(&bad_z.clone()).is_err()); + + let pedersen_params = Pedersen::::new_params(&mut rng, ccs.n - ccs.l - 1); + // Compute v_j with the right z + let (lcccs, _) = ccs.to_lcccs(&mut rng, &pedersen_params, &z); + // with our test vector comming from R1CS, v should have length 3 + assert_eq!(lcccs.v.len(), 3); + + // Bad compute L_j(x) with the bad z + let vec_L_j_x = lcccs.compute_Ls(&ccs, &bad_z); + assert_eq!(vec_L_j_x.len(), lcccs.v.len()); + + // 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(Fr::zero(), |acc, result| acc + result); + if v_i != sum_L_j_x { + satisfied = false; + } + } + + assert!(!satisfied); + } +} diff --git a/src/folding/hypernova/mod.rs b/src/folding/hypernova/mod.rs new file mode 100644 index 0000000..c890b96 --- /dev/null +++ b/src/folding/hypernova/mod.rs @@ -0,0 +1,5 @@ +/// Implements the scheme described in [HyperNova](https://eprint.iacr.org/2023/573.pdf) +pub mod cccs; +pub mod lcccs; +pub mod nimfs; +pub mod utils; diff --git a/src/folding/hypernova/nimfs.rs b/src/folding/hypernova/nimfs.rs new file mode 100644 index 0000000..b824c39 --- /dev/null +++ b/src/folding/hypernova/nimfs.rs @@ -0,0 +1,657 @@ +use ark_ec::CurveGroup; +use ark_ff::{Field, PrimeField}; +use ark_std::{One, Zero}; + +use espresso_subroutines::PolyIOP; +use espresso_transcript::IOPTranscript; + +use super::cccs::{Witness, CCCS}; +use super::lcccs::LCCCS; +use super::utils::{compute_c_from_sigmas_and_thetas, compute_g, compute_sigmas_and_thetas}; +use crate::ccs::CCS; +use crate::utils::hypercube::BooleanHypercube; +use crate::utils::sum_check::structs::IOPProof as SumCheckProof; +use crate::utils::sum_check::{verifier::interpolate_uni_poly, SumCheck}; +use crate::utils::virtual_polynomial::VPAuxInfo; + +use std::marker::PhantomData; + +/// Proof defines a multifolding proof +#[derive(Debug)] +pub struct Proof { + pub sc_proof: SumCheckProof, + pub sigmas_thetas: SigmasThetas, +} + +#[derive(Debug)] +pub struct SigmasThetas(pub Vec>, pub Vec>); + +#[derive(Debug)] +/// Implements the Non-Interactive Multi Folding Scheme described in section 5 of +/// [HyperNova](https://eprint.iacr.org/2023/573.pdf) +pub struct NIMFS { + pub _c: PhantomData, +} + +impl NIMFS { + pub fn fold( + lcccs: &[LCCCS], + cccs: &[CCCS], + sigmas_thetas: &SigmasThetas, + r_x_prime: Vec, + rho: C::ScalarField, + ) -> LCCCS { + let (sigmas, thetas) = (sigmas_thetas.0.clone(), sigmas_thetas.1.clone()); + let mut C_folded = C::zero(); + let mut u_folded = C::ScalarField::zero(); + let mut x_folded: Vec = vec![C::ScalarField::zero(); lcccs[0].x.len()]; + let mut v_folded: Vec = vec![C::ScalarField::zero(); sigmas[0].len()]; + + for i in 0..(lcccs.len() + cccs.len()) { + let rho_i = rho.pow([i as u64]); + + let c: C; + let u: C::ScalarField; + let x: Vec; + let v: Vec; + if i < lcccs.len() { + c = lcccs[i].C; + u = lcccs[i].u; + x = lcccs[i].x.clone(); + v = sigmas[i].clone(); + } else { + c = cccs[i - lcccs.len()].C; + u = C::ScalarField::one(); + x = cccs[i - lcccs.len()].x.clone(); + v = thetas[i - lcccs.len()].clone(); + } + + C_folded += c.mul(rho_i); + u_folded += rho_i * u; + x_folded = x_folded + .iter() + .zip( + x.iter() + .map(|x_i| *x_i * rho_i) + .collect::>(), + ) + .map(|(a_i, b_i)| *a_i + b_i) + .collect(); + + v_folded = v_folded + .iter() + .zip( + v.iter() + .map(|x_i| *x_i * rho_i) + .collect::>(), + ) + .map(|(a_i, b_i)| *a_i + b_i) + .collect(); + } + + LCCCS:: { + C: C_folded, + u: u_folded, + x: x_folded, + r_x: r_x_prime, + v: v_folded, + } + } + + pub fn fold_witness( + w_lcccs: &[Witness], + w_cccs: &[Witness], + rho: C::ScalarField, + ) -> Witness { + let mut w_folded: Vec = vec![C::ScalarField::zero(); w_lcccs[0].w.len()]; + let mut r_w_folded = C::ScalarField::zero(); + + for i in 0..(w_lcccs.len() + w_cccs.len()) { + let rho_i = rho.pow([i as u64]); + let w: Vec; + let r_w: C::ScalarField; + + if i < w_lcccs.len() { + w = w_lcccs[i].w.clone(); + r_w = w_lcccs[i].r_w; + } else { + w = w_cccs[i - w_lcccs.len()].w.clone(); + r_w = w_cccs[i - w_lcccs.len()].r_w; + } + + w_folded = w_folded + .iter() + .zip( + w.iter() + .map(|x_i| *x_i * rho_i) + .collect::>(), + ) + .map(|(a_i, b_i)| *a_i + b_i) + .collect(); + + r_w_folded += rho_i * r_w; + } + Witness { + w: w_folded, + r_w: r_w_folded, + } + } + + /// Performs the multifolding prover. Given μ LCCCS instances and ν CCS instances, fold them + /// into a single LCCCS instance. Since this is the prover, also fold their witness. + /// Returns the final folded LCCCS, the folded witness, the sumcheck proof, and the helper + /// sumcheck claim sigmas and thetas. + pub fn prove( + transcript: &mut IOPTranscript, + ccs: &CCS, + running_instances: &[LCCCS], + new_instances: &[CCCS], + w_lcccs: &[Witness], + w_cccs: &[Witness], + ) -> (Proof, LCCCS, Witness) { + // TODO appends to transcript + + assert!(!running_instances.is_empty()); + assert!(!new_instances.is_empty()); + + // construct the LCCCS z vector from the relaxation factor, public IO and witness + // XXX this deserves its own function in LCCCS + let mut z_lcccs = Vec::new(); + for (i, running_instance) in running_instances.iter().enumerate() { + let z_1: Vec = [ + vec![running_instance.u], + running_instance.x.clone(), + w_lcccs[i].w.to_vec(), + ] + .concat(); + z_lcccs.push(z_1); + } + // construct the CCCS z vector from the public IO and witness + let mut z_cccs = Vec::new(); + for (i, new_instance) in new_instances.iter().enumerate() { + let z_2: Vec = [ + vec![C::ScalarField::one()], + new_instance.x.clone(), + w_cccs[i].w.to_vec(), + ] + .concat(); + z_cccs.push(z_2); + } + + // Step 1: Get some challenges + let gamma: C::ScalarField = transcript.get_and_append_challenge(b"gamma").unwrap(); + let beta: Vec = transcript + .get_and_append_challenge_vectors(b"beta", ccs.s) + .unwrap(); + + // Compute g(x) + let g = compute_g(ccs, running_instances, &z_lcccs, &z_cccs, gamma, &beta); + + // Step 3: Run the sumcheck prover + let sumcheck_proof = + as SumCheck>::prove(&g, transcript).unwrap(); // XXX unwrap + + // Note: The following two "sanity checks" are done for this prototype, in a final version + // they should be removed. + // + // Sanity check 1: evaluate g(x) over x \in {0,1} (the boolean hypercube), and check that + // its sum is equal to the extracted_sum from the SumCheck. + ////////////////////////////////////////////////////////////////////// + let mut g_over_bhc = C::ScalarField::zero(); + for x in BooleanHypercube::new(ccs.s) { + g_over_bhc += g.evaluate(&x).unwrap(); + } + + // note: this is the sum of g(x) over the whole boolean hypercube + let extracted_sum = + as SumCheck>::extract_sum(&sumcheck_proof); + assert_eq!(extracted_sum, g_over_bhc); + // Sanity check 2: expect \sum v_j * gamma^j to be equal to the sum of g(x) over the + // boolean hypercube (and also equal to the extracted_sum from the SumCheck). + let mut sum_v_j_gamma = C::ScalarField::zero(); + for (i, running_instance) in running_instances.iter().enumerate() { + for j in 0..running_instance.v.len() { + let gamma_j = gamma.pow([(i * ccs.t + j) as u64]); + sum_v_j_gamma += running_instance.v[j] * gamma_j; + } + } + assert_eq!(g_over_bhc, sum_v_j_gamma); + assert_eq!(extracted_sum, sum_v_j_gamma); + ////////////////////////////////////////////////////////////////////// + + // Step 2: dig into the sumcheck and extract r_x_prime + let r_x_prime = sumcheck_proof.point.clone(); + + // Step 4: compute sigmas and thetas + let sigmas_thetas = compute_sigmas_and_thetas(ccs, &z_lcccs, &z_cccs, &r_x_prime); + + // Step 6: Get the folding challenge + let rho: C::ScalarField = transcript.get_and_append_challenge(b"rho").unwrap(); + + // Step 7: Create the folded instance + let folded_lcccs = Self::fold( + running_instances, + new_instances, + &sigmas_thetas, + r_x_prime, + rho, + ); + + // Step 8: Fold the witnesses + let folded_witness = Self::fold_witness(w_lcccs, w_cccs, rho); + + ( + Proof:: { + sc_proof: sumcheck_proof, + sigmas_thetas, + }, + folded_lcccs, + folded_witness, + ) + } + + /// Performs the multifolding verifier. Given μ LCCCS instances and ν CCS instances, fold them + /// into a single LCCCS instance. + /// Returns the folded LCCCS instance. + pub fn verify( + transcript: &mut IOPTranscript, + ccs: &CCS, + running_instances: &[LCCCS], + new_instances: &[CCCS], + proof: Proof, + ) -> LCCCS { + // TODO appends to transcript + + assert!(!running_instances.is_empty()); + assert!(!new_instances.is_empty()); + + // Step 1: Get some challenges + let gamma: C::ScalarField = transcript.get_and_append_challenge(b"gamma").unwrap(); + let beta: Vec = transcript + .get_and_append_challenge_vectors(b"beta", ccs.s) + .unwrap(); + + let vp_aux_info = VPAuxInfo:: { + max_degree: ccs.d + 1, + num_variables: ccs.s, + phantom: PhantomData::, + }; + + // Step 3: Start verifying the sumcheck + // First, compute the expected sumcheck sum: \sum gamma^j v_j + let mut sum_v_j_gamma = C::ScalarField::zero(); + for (i, running_instance) in running_instances.iter().enumerate() { + for j in 0..running_instance.v.len() { + let gamma_j = gamma.pow([(i * ccs.t + j) as u64]); + sum_v_j_gamma += running_instance.v[j] * gamma_j; + } + } + + // Verify the interactive part of the sumcheck + let sumcheck_subclaim = as SumCheck>::verify( + sum_v_j_gamma, + &proof.sc_proof, + &vp_aux_info, + transcript, + ) + .unwrap(); + + // Step 2: Dig into the sumcheck claim and extract the randomness used + let r_x_prime = sumcheck_subclaim.point.clone(); + + // Step 5: Finish verifying sumcheck (verify the claim c) + let c = compute_c_from_sigmas_and_thetas( + ccs, + &proof.sigmas_thetas, + gamma, + &beta, + &running_instances + .iter() + .map(|lcccs| lcccs.r_x.clone()) + .collect(), + &r_x_prime, + ); + // check that the g(r_x') from the sumcheck proof is equal to the computed c from sigmas&thetas + assert_eq!(c, sumcheck_subclaim.expected_evaluation); + + // Sanity check: we can also compute g(r_x') from the proof last evaluation value, and + // should be equal to the previously obtained values. + let g_on_rxprime_from_sumcheck_last_eval = interpolate_uni_poly::( + &proof.sc_proof.proofs.last().unwrap().evaluations, + *r_x_prime.last().unwrap(), + ) + .unwrap(); + assert_eq!(g_on_rxprime_from_sumcheck_last_eval, c); + assert_eq!( + g_on_rxprime_from_sumcheck_last_eval, + sumcheck_subclaim.expected_evaluation + ); + + // Step 6: Get the folding challenge + let rho: C::ScalarField = transcript.get_and_append_challenge(b"rho").unwrap(); + + // Step 7: Compute the folded instance + Self::fold( + running_instances, + new_instances, + &proof.sigmas_thetas, + r_x_prime, + rho, + ) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::ccs::tests::{get_test_ccs, get_test_z}; + use ark_std::test_rng; + use ark_std::UniformRand; + + use crate::pedersen::Pedersen; + use ark_pallas::{Fr, Projective}; + + #[test] + fn test_fold() { + let ccs = get_test_ccs(); + let z1 = get_test_z(3); + let z2 = get_test_z(4); + ccs.check_relation(&z1).unwrap(); + ccs.check_relation(&z2).unwrap(); + + let mut rng = test_rng(); + let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + + let sigmas_thetas = + compute_sigmas_and_thetas(&ccs, &[z1.clone()], &[z2.clone()], &r_x_prime); + + let pedersen_params = Pedersen::::new_params(&mut rng, ccs.n - ccs.l - 1); + + let (lcccs, w1) = ccs.to_lcccs(&mut rng, &pedersen_params, &z1); + let (cccs, w2) = ccs.to_cccs(&mut rng, &pedersen_params, &z2); + + lcccs.check_relation(&pedersen_params, &ccs, &w1).unwrap(); + cccs.check_relation(&pedersen_params, &ccs, &w2).unwrap(); + + let mut rng = test_rng(); + let rho = Fr::rand(&mut rng); + + let folded = NIMFS::::fold(&[lcccs], &[cccs], &sigmas_thetas, r_x_prime, rho); + + let w_folded = NIMFS::::fold_witness(&[w1], &[w2], rho); + + // check lcccs relation + folded + .check_relation(&pedersen_params, &ccs, &w_folded) + .unwrap(); + } + + /// Perform multifolding of an LCCCS instance with a CCCS instance (as described in the paper) + #[test] + pub fn test_basic_multifolding() { + let mut rng = test_rng(); + + // Create a basic CCS circuit + let ccs = get_test_ccs::(); + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + + // Generate a satisfying witness + let z_1 = get_test_z(3); + // Generate another satisfying witness + let z_2 = get_test_z(4); + + // Create the LCCCS instance out of z_1 + let (running_instance, w1) = ccs.to_lcccs(&mut rng, &pedersen_params, &z_1); + // Create the CCCS instance out of z_2 + let (new_instance, w2) = ccs.to_cccs(&mut rng, &pedersen_params, &z_2); + + // Prover's transcript + let mut transcript_p = IOPTranscript::::new(b"multifolding"); + transcript_p.append_message(b"init", b"init").unwrap(); + + // Run the prover side of the multifolding + let (proof, folded_lcccs, folded_witness) = NIMFS::::prove( + &mut transcript_p, + &ccs, + &[running_instance.clone()], + &[new_instance.clone()], + &[w1], + &[w2], + ); + + // Verifier's transcript + let mut transcript_v = IOPTranscript::::new(b"multifolding"); + transcript_v.append_message(b"init", b"init").unwrap(); + + // Run the verifier side of the multifolding + let folded_lcccs_v = NIMFS::::verify( + &mut transcript_v, + &ccs, + &[running_instance.clone()], + &[new_instance.clone()], + proof, + ); + assert_eq!(folded_lcccs, folded_lcccs_v); + + // Check that the folded LCCCS instance is a valid instance with respect to the folded witness + folded_lcccs + .check_relation(&pedersen_params, &ccs, &folded_witness) + .unwrap(); + } + + /// Perform multiple steps of multifolding of an LCCCS instance with a CCCS instance + #[test] + pub fn test_multifolding_two_instances_multiple_steps() { + let mut rng = test_rng(); + + let ccs = get_test_ccs::(); + + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + + // LCCCS witness + let z_1 = get_test_z(2); + let (mut running_instance, mut w1) = ccs.to_lcccs(&mut rng, &pedersen_params, &z_1); + + let mut transcript_p = IOPTranscript::::new(b"multifolding"); + let mut transcript_v = IOPTranscript::::new(b"multifolding"); + transcript_p.append_message(b"init", b"init").unwrap(); + transcript_v.append_message(b"init", b"init").unwrap(); + + let n: usize = 10; + for i in 3..n { + println!("\niteration: i {}", i); // DBG + + // CCS witness + let z_2 = get_test_z(i); + println!("z_2 {:?}", z_2); // DBG + + let (new_instance, w2) = ccs.to_cccs(&mut rng, &pedersen_params, &z_2); + + // run the prover side of the multifolding + let (proof, folded_lcccs, folded_witness) = NIMFS::::prove( + &mut transcript_p, + &ccs, + &[running_instance.clone()], + &[new_instance.clone()], + &[w1], + &[w2], + ); + + // run the verifier side of the multifolding + let folded_lcccs_v = NIMFS::::verify( + &mut transcript_v, + &ccs, + &[running_instance.clone()], + &[new_instance.clone()], + proof, + ); + + assert_eq!(folded_lcccs, folded_lcccs_v); + + // check that the folded instance with the folded witness holds the LCCCS relation + println!("check_relation {}", i); + folded_lcccs + .check_relation(&pedersen_params, &ccs, &folded_witness) + .unwrap(); + + running_instance = folded_lcccs; + w1 = folded_witness; + } + } + + /// Test that generates mu>1 and nu>1 instances, and folds them in a single multifolding step. + #[test] + pub fn test_multifolding_mu_nu_instances() { + let mut rng = test_rng(); + + // Create a basic CCS circuit + let ccs = get_test_ccs::(); + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + + let mu = 10; + let nu = 15; + + // Generate a mu LCCCS & nu CCCS satisfying witness + let mut z_lcccs = Vec::new(); + for i in 0..mu { + let z = get_test_z(i + 3); + z_lcccs.push(z); + } + let mut z_cccs = Vec::new(); + for i in 0..nu { + let z = get_test_z(nu + i + 3); + z_cccs.push(z); + } + + // Create the LCCCS instances out of z_lcccs + let mut lcccs_instances = Vec::new(); + let mut w_lcccs = Vec::new(); + for z_i in z_lcccs.iter() { + let (running_instance, w) = ccs.to_lcccs(&mut rng, &pedersen_params, z_i); + lcccs_instances.push(running_instance); + w_lcccs.push(w); + } + // Create the CCCS instance out of z_cccs + let mut cccs_instances = Vec::new(); + let mut w_cccs = Vec::new(); + for z_i in z_cccs.iter() { + let (new_instance, w) = ccs.to_cccs(&mut rng, &pedersen_params, z_i); + cccs_instances.push(new_instance); + w_cccs.push(w); + } + + // Prover's transcript + let mut transcript_p = IOPTranscript::::new(b"multifolding"); + transcript_p.append_message(b"init", b"init").unwrap(); + + // Run the prover side of the multifolding + let (proof, folded_lcccs, folded_witness) = NIMFS::::prove( + &mut transcript_p, + &ccs, + &lcccs_instances, + &cccs_instances, + &w_lcccs, + &w_cccs, + ); + + // Verifier's transcript + let mut transcript_v = IOPTranscript::::new(b"multifolding"); + transcript_v.append_message(b"init", b"init").unwrap(); + + // Run the verifier side of the multifolding + let folded_lcccs_v = NIMFS::::verify( + &mut transcript_v, + &ccs, + &lcccs_instances, + &cccs_instances, + proof, + ); + assert_eq!(folded_lcccs, folded_lcccs_v); + + // Check that the folded LCCCS instance is a valid instance with respect to the folded witness + folded_lcccs + .check_relation(&pedersen_params, &ccs, &folded_witness) + .unwrap(); + } + + /// Test that generates mu>1 and nu>1 instances, and folds them in a single multifolding step + /// and repeats the process doing multiple steps. + #[test] + pub fn test_multifolding_mu_nu_instances_multiple_steps() { + let mut rng = test_rng(); + + // Create a basic CCS circuit + let ccs = get_test_ccs::(); + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + + // Prover's transcript + let mut transcript_p = IOPTranscript::::new(b"multifolding"); + transcript_p.append_message(b"init", b"init").unwrap(); + + // Verifier's transcript + let mut transcript_v = IOPTranscript::::new(b"multifolding"); + transcript_v.append_message(b"init", b"init").unwrap(); + + let n_steps = 3; + + // number of LCCCS & CCCS instances in each multifolding step + let mu = 10; + let nu = 15; + + // Generate a mu LCCCS & nu CCCS satisfying witness, for each step + for step in 0..n_steps { + let mut z_lcccs = Vec::new(); + for i in 0..mu { + let z = get_test_z(step + i + 3); + z_lcccs.push(z); + } + let mut z_cccs = Vec::new(); + for i in 0..nu { + let z = get_test_z(nu + i + 3); + z_cccs.push(z); + } + + // Create the LCCCS instances out of z_lcccs + let mut lcccs_instances = Vec::new(); + let mut w_lcccs = Vec::new(); + for z_i in z_lcccs.iter() { + let (running_instance, w) = ccs.to_lcccs(&mut rng, &pedersen_params, z_i); + lcccs_instances.push(running_instance); + w_lcccs.push(w); + } + // Create the CCCS instance out of z_cccs + let mut cccs_instances = Vec::new(); + let mut w_cccs = Vec::new(); + for z_i in z_cccs.iter() { + let (new_instance, w) = ccs.to_cccs(&mut rng, &pedersen_params, z_i); + cccs_instances.push(new_instance); + w_cccs.push(w); + } + + // Run the prover side of the multifolding + let (proof, folded_lcccs, folded_witness) = NIMFS::::prove( + &mut transcript_p, + &ccs, + &lcccs_instances, + &cccs_instances, + &w_lcccs, + &w_cccs, + ); + + // Run the verifier side of the multifolding + let folded_lcccs_v = NIMFS::::verify( + &mut transcript_v, + &ccs, + &lcccs_instances, + &cccs_instances, + proof, + ); + assert_eq!(folded_lcccs, folded_lcccs_v); + + // Check that the folded LCCCS instance is a valid instance with respect to the folded witness + folded_lcccs + .check_relation(&pedersen_params, &ccs, &folded_witness) + .unwrap(); + } + } +} diff --git a/src/folding/hypernova/utils.rs b/src/folding/hypernova/utils.rs new file mode 100644 index 0000000..23bd50f --- /dev/null +++ b/src/folding/hypernova/utils.rs @@ -0,0 +1,360 @@ +use ark_ec::CurveGroup; +use ark_ff::{Field, PrimeField}; +use ark_poly::DenseMultilinearExtension; +use ark_poly::MultilinearExtension; +use ark_std::{One, Zero}; +use std::ops::Add; + +use crate::utils::multilinear_polynomial::fix_variables; +use crate::utils::multilinear_polynomial::scalar_mul; + +use super::lcccs::LCCCS; +use super::nimfs::SigmasThetas; +use crate::ccs::CCS; +use crate::utils::hypercube::BooleanHypercube; +use crate::utils::mle::dense_vec_to_mle; +use crate::utils::mle::matrix_to_mle; +use crate::utils::vec::SparseMatrix; +use crate::utils::virtual_polynomial::{eq_eval, VirtualPolynomial}; + +/// 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( + vec_M: &[SparseMatrix], + z: &Vec, + r: &[F], + s_prime: usize, +) -> Vec { + // Convert z to MLE + let z_y_mle = dense_vec_to_mle(s_prime, z); + // Convert all matrices to MLE + let M_x_y_mle: Vec> = + vec_M.iter().cloned().map(matrix_to_mle).collect(); + + 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, s_prime); + let v_i = sum_Mz.evaluate(r).unwrap(); + v.push(v_i); + } + v +} + +/// Return the multilinear polynomial p(x) = \sum_{y \in {0,1}^s'} M_j(x, y) * z(y) +pub fn compute_sum_Mz( + M_j: DenseMultilinearExtension, + z: &DenseMultilinearExtension, + s_prime: usize, +) -> DenseMultilinearExtension { + let mut sum_Mz = DenseMultilinearExtension { + evaluations: vec![F::zero(); M_j.evaluations.len()], + num_vars: M_j.num_vars - s_prime, + }; + + let bhc = BooleanHypercube::new(s_prime); + for y in bhc.into_iter() { + // In a slightly counter-intuitive fashion fix_variables() fixes the right-most variables of the polynomial. So + // for a polynomial M(x,y) and a random field element r, if we do fix_variables(M,r) we will get M(x,r). + let M_j_y = fix_variables(&M_j, &y); + let z_y = z.evaluate(&y).unwrap(); + let M_j_z = scalar_mul(&M_j_y, &z_y); + sum_Mz = sum_Mz.add(M_j_z); + } + sum_Mz +} + +/// Compute the arrays of sigma_i and theta_i from step 4 corresponding to the LCCCS and CCCS +/// instances +pub fn compute_sigmas_and_thetas( + ccs: &CCS, + z_lcccs: &[Vec], + z_cccs: &[Vec], + r_x_prime: &[C::ScalarField], +) -> SigmasThetas { + let mut sigmas: Vec> = Vec::new(); + for z_lcccs_i in z_lcccs { + // sigmas + let sigma_i = compute_all_sum_Mz_evals(&ccs.M, z_lcccs_i, r_x_prime, ccs.s_prime); + sigmas.push(sigma_i); + } + let mut thetas: Vec> = Vec::new(); + for z_cccs_i in z_cccs { + // thetas + let theta_i = compute_all_sum_Mz_evals(&ccs.M, z_cccs_i, r_x_prime, ccs.s_prime); + thetas.push(theta_i); + } + SigmasThetas(sigmas, thetas) +} + +/// Compute the right-hand-side of step 5 of the multifolding scheme +pub fn compute_c_from_sigmas_and_thetas( + ccs: &CCS, + st: &SigmasThetas, + gamma: C::ScalarField, + beta: &[C::ScalarField], + vec_r_x: &Vec>, + r_x_prime: &[C::ScalarField], +) -> C::ScalarField { + let (vec_sigmas, vec_thetas) = (st.0.clone(), st.1.clone()); + let mut c = C::ScalarField::zero(); + + let mut e_lcccs = Vec::new(); + for r_x in vec_r_x { + e_lcccs.push(eq_eval(r_x, r_x_prime).unwrap()); + } + for (i, sigmas) in vec_sigmas.iter().enumerate() { + // (sum gamma^j * e_i * sigma_j) + for (j, sigma_j) in sigmas.iter().enumerate() { + let gamma_j = gamma.pow([(i * ccs.t + j) as u64]); + c += gamma_j * e_lcccs[i] * sigma_j; + } + } + + let mu = vec_sigmas.len(); + let e2 = eq_eval(beta, r_x_prime).unwrap(); + for (k, thetas) in vec_thetas.iter().enumerate() { + // + gamma^{t+1} * e2 * sum c_i * prod theta_j + let mut lhs = C::ScalarField::zero(); + for i in 0..ccs.q { + let mut prod = C::ScalarField::one(); + for j in ccs.S[i].clone() { + prod *= thetas[j]; + } + lhs += ccs.c[i] * prod; + } + let gamma_t1 = gamma.pow([(mu * ccs.t + k) as u64]); + c += gamma_t1 * e2 * lhs; + } + c +} + +/// Compute g(x) polynomial for the given inputs. +pub fn compute_g( + ccs: &CCS, + running_instances: &[LCCCS], + z_lcccs: &[Vec], + z_cccs: &[Vec], + gamma: C::ScalarField, + beta: &[C::ScalarField], +) -> VirtualPolynomial { + let mu = running_instances.len(); + let mut vec_Ls: Vec> = Vec::new(); + for (i, running_instance) in running_instances.iter().enumerate() { + let mut Ls = running_instance.compute_Ls(ccs, &z_lcccs[i]); + vec_Ls.append(&mut Ls); + } + let mut vec_Q: Vec> = Vec::new(); + // for (i, _) in cccs_instances.iter().enumerate() { + for z_cccs_i in z_cccs.iter() { + let Q = ccs.compute_Q(z_cccs_i, beta); + vec_Q.push(Q); + } + let mut g = vec_Ls[0].clone(); + + // note: the following two loops can be integrated in the previous two loops, but left + // separated for clarity in the PoC implementation. + for (j, L_j) in vec_Ls.iter_mut().enumerate().skip(1) { + let gamma_j = gamma.pow([j as u64]); + L_j.scalar_mul(&gamma_j); + g = g.add(L_j); + } + for (i, Q_i) in vec_Q.iter_mut().enumerate() { + let gamma_mut_i = gamma.pow([(mu * ccs.t + i) as u64]); + Q_i.scalar_mul(&gamma_mut_i); + g = g.add(Q_i); + } + g +} + +#[cfg(test)] +pub mod tests { + use super::*; + + use ark_pallas::{Fr, Projective}; + use ark_std::test_rng; + use ark_std::One; + use ark_std::UniformRand; + use ark_std::Zero; + + use crate::ccs::tests::{get_test_ccs, get_test_z}; + use crate::pedersen::Pedersen; + use crate::utils::multilinear_polynomial::tests::fix_last_variables; + use crate::utils::virtual_polynomial::eq_eval; + + #[test] + fn test_compute_sum_Mz_over_boolean_hypercube() { + let ccs = get_test_ccs::(); + let z = get_test_z(3); + ccs.check_relation(&z).unwrap(); + let z_mle = dense_vec_to_mle(ccs.s_prime, &z); + + // check that evaluating over all the values x over the boolean hypercube, the result of + // the next for loop is equal to 0 + for x in BooleanHypercube::new(ccs.s) { + let mut r = Fr::zero(); + for i in 0..ccs.q { + let mut Sj_prod = Fr::one(); + for j in ccs.S[i].clone() { + let M_j = matrix_to_mle(ccs.M[j].clone()); + let sum_Mz = compute_sum_Mz(M_j, &z_mle, ccs.s_prime); + let sum_Mz_x = sum_Mz.evaluate(&x).unwrap(); + Sj_prod *= sum_Mz_x; + } + r += Sj_prod * ccs.c[i]; + } + assert_eq!(r, Fr::zero()); + } + } + + /// Given M(x,y) matrix and a random field element `r`, test that ~M(r,y) is is an s'-variable polynomial which + /// compresses every column j of the M(x,y) matrix by performing a random linear combination between the elements + /// of the column and the values eq_i(r) where i is the row of that element + /// + /// For example, for matrix M: + /// + /// [2, 3, 4, 4 + /// 4, 4, 3, 2 + /// 2, 8, 9, 2 + /// 9, 4, 2, 0] + /// + /// The polynomial ~M(r,y) is a polynomial in F^2 which evaluates to the following values in the hypercube: + /// - M(00) = 2*eq_00(r) + 4*eq_10(r) + 2*eq_01(r) + 9*eq_11(r) + /// - M(10) = 3*eq_00(r) + 4*eq_10(r) + 8*eq_01(r) + 4*eq_11(r) + /// - M(01) = 4*eq_00(r) + 3*eq_10(r) + 9*eq_01(r) + 2*eq_11(r) + /// - M(11) = 4*eq_00(r) + 2*eq_10(r) + 2*eq_01(r) + 0*eq_11(r) + /// + /// This is used by Hypernova in LCCCS to perform a verifier-chosen random linear combination between the columns + /// of the matrix and the z vector. This technique is also used extensively in "An Algebraic Framework for + /// Universal and Updatable SNARKs". + #[test] + fn test_compute_M_r_y_compression() { + let mut rng = test_rng(); + + // s = 2, s' = 3 + let ccs = get_test_ccs::(); + + let M = ccs.M[0].clone().to_dense(); + let M_mle = matrix_to_mle(ccs.M[0].clone()); + + // Fix the polynomial ~M(r,y) + let r: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + let M_r_y = fix_last_variables(&M_mle, &r); + + // compute M_r_y the other way around + for j in 0..M[0].len() { + // Go over every column of M + let column_j: Vec = M.clone().iter().map(|x| x[j]).collect(); + // and perform the random lincomb between the elements of the column and eq_i(r) + let rlc = BooleanHypercube::new(ccs.s) + .enumerate() + .map(|(i, x)| column_j[i] * eq_eval(&x, &r).unwrap()) + .fold(Fr::zero(), |acc, result| acc + result); + + assert_eq!(M_r_y.evaluations[j], rlc); + } + } + + #[test] + fn test_compute_sigmas_and_thetas() { + let ccs = get_test_ccs(); + let z1 = get_test_z(3); + let z2 = get_test_z(4); + ccs.check_relation(&z1).unwrap(); + ccs.check_relation(&z2).unwrap(); + + let mut rng = test_rng(); + let gamma: Fr = Fr::rand(&mut rng); + let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + + // Initialize a multifolding object + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + let (lcccs_instance, _) = ccs.to_lcccs(&mut rng, &pedersen_params, &z1); + + let sigmas_thetas = + compute_sigmas_and_thetas(&ccs, &[z1.clone()], &[z2.clone()], &r_x_prime); + + let g = compute_g( + &ccs, + &[lcccs_instance.clone()], + &[z1.clone()], + &[z2.clone()], + gamma, + &beta, + ); + + // 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(&r_x_prime).unwrap(); + let c = compute_c_from_sigmas_and_thetas::( + &ccs, + &sigmas_thetas, + gamma, + &beta, + &vec![lcccs_instance.r_x], + &r_x_prime, + ); + assert_eq!(c, expected_c); + } + + #[test] + fn test_compute_g() { + let ccs = get_test_ccs(); + let z1 = get_test_z(3); + let z2 = get_test_z(4); + ccs.check_relation(&z1).unwrap(); + ccs.check_relation(&z2).unwrap(); + + let mut rng = test_rng(); // TMP + let gamma: Fr = Fr::rand(&mut rng); + let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + + // Initialize a multifolding object + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + let (lcccs_instance, _) = ccs.to_lcccs(&mut rng, &pedersen_params, &z1); + + let mut sum_v_j_gamma = Fr::zero(); + for j in 0..lcccs_instance.v.len() { + let gamma_j = gamma.pow([j as u64]); + sum_v_j_gamma += lcccs_instance.v[j] * gamma_j; + } + + // Compute g(x) with that r_x + let g = compute_g::( + &ccs, + &[lcccs_instance.clone()], + &[z1.clone()], + &[z2.clone()], + gamma, + &beta, + ); + + // evaluate g(x) over x \in {0,1}^s + let mut g_on_bhc = Fr::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 = Fr::zero(); + let vec_L = lcccs_instance.compute_Ls(&ccs, &z1); + for x in BooleanHypercube::new(ccs.s) { + for (j, Lj) in vec_L.iter().enumerate() { + let gamma_j = gamma.pow([j as u64]); + sum_Lj_on_bhc += Lj.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, Fr::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); + } +} diff --git a/src/folding/mod.rs b/src/folding/mod.rs index 92d05ce..7bafdf5 100644 --- a/src/folding/mod.rs +++ b/src/folding/mod.rs @@ -1,2 +1,5 @@ pub mod circuits; +#[cfg(feature = "hypernova")] +pub mod hypernova; +#[cfg(feature = "nova")] pub mod nova; diff --git a/src/folding/nova/mod.rs b/src/folding/nova/mod.rs index 33fe2d0..1ec59f3 100644 --- a/src/folding/nova/mod.rs +++ b/src/folding/nova/mod.rs @@ -1,3 +1,4 @@ +/// Implements the scheme described in [Nova](https://eprint.iacr.org/2021/370.pdf) use ark_crypto_primitives::sponge::Absorb; use ark_ec::{CurveGroup, Group}; use ark_std::fmt::Debug; diff --git a/src/folding/nova/nifs.rs b/src/folding/nova/nifs.rs index b48d2b8..d211933 100644 --- a/src/folding/nova/nifs.rs +++ b/src/folding/nova/nifs.rs @@ -9,6 +9,8 @@ use crate::pedersen::{Params as PedersenParams, Pedersen, Proof as PedersenProof use crate::transcript::Transcript; use crate::utils::vec::*; +/// Implements the Non-Interactive Folding Scheme described in section 4 of +/// https://eprint.iacr.org/2021/370.pdf pub struct NIFS { _phantom: PhantomData, } diff --git a/src/pedersen.rs b/src/pedersen.rs index 968cd4c..16af2e5 100644 --- a/src/pedersen.rs +++ b/src/pedersen.rs @@ -1,11 +1,10 @@ -use ark_ec::{CurveGroup, Group}; +use ark_ec::CurveGroup; use ark_std::{rand::Rng, UniformRand}; use std::marker::PhantomData; use crate::utils::vec::{vec_add, vec_scalar_mul}; use crate::transcript::Transcript; -use ark_crypto_primitives::sponge::Absorb; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Proof { @@ -21,17 +20,11 @@ pub struct Params { } #[derive(Debug, Clone, Eq, PartialEq)] -pub struct Pedersen -where - ::ScalarField: Absorb, -{ +pub struct Pedersen { _c: PhantomData, } -impl Pedersen -where - ::ScalarField: Absorb, -{ +impl Pedersen { pub fn new_params(rng: &mut R, max: usize) -> Params { let generators: Vec = std::iter::repeat_with(|| C::Affine::rand(rng)) .take(max.next_power_of_two()) diff --git a/src/utils/espresso/sum_check/verifier.rs b/src/utils/espresso/sum_check/verifier.rs index ef4bf76..e9b4470 100644 --- a/src/utils/espresso/sum_check/verifier.rs +++ b/src/utils/espresso/sum_check/verifier.rs @@ -319,7 +319,7 @@ fn u64_factorial(a: usize) -> u64 { } #[cfg(test)] -mod test { +mod tests { use super::interpolate_uni_poly; use ark_pallas::Fr; use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; diff --git a/src/utils/espresso/virtual_polynomial.rs b/src/utils/espresso/virtual_polynomial.rs index bbd65d7..6017a73 100644 --- a/src/utils/espresso/virtual_polynomial.rs +++ b/src/utils/espresso/virtual_polynomial.rs @@ -409,7 +409,7 @@ pub fn bit_decompose(input: u64, num_var: usize) -> Vec { } #[cfg(test)] -mod test { +mod tests { use super::*; use crate::utils::multilinear_polynomial::tests::random_mle_list; use ark_ff::UniformRand; diff --git a/src/utils/hypercube.rs b/src/utils/hypercube.rs new file mode 100644 index 0000000..673df1a --- /dev/null +++ b/src/utils/hypercube.rs @@ -0,0 +1,77 @@ +/// A boolean hypercube structure to create an ergonomic evaluation domain +use crate::utils::virtual_polynomial::bit_decompose; +use ark_ff::PrimeField; + +use std::marker::PhantomData; + +/// A boolean hypercube that returns its points as an iterator +/// If you iterate on it for 3 variables you will get points in little-endian order: +/// 000 -> 100 -> 010 -> 110 -> 001 -> 101 -> 011 -> 111 +#[derive(Debug, Clone)] +pub struct BooleanHypercube { + _f: PhantomData, + n_vars: usize, + current: u64, + max: u64, +} + +impl BooleanHypercube { + pub fn new(n_vars: usize) -> Self { + BooleanHypercube:: { + _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 little-endian bit representation of i) + pub fn at_i(&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)).collect() + } +} + +impl Iterator for BooleanHypercube { + type Item = Vec; + + fn next(&mut self) -> Option { + let bits = bit_decompose(self.current, self.n_vars); + let result: Vec = bits.iter().map(|&x| F::from(x)).collect(); + self.current += 1; + + if self.current > self.max { + return None; + } + + Some(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::vec::tests::to_F_dense_matrix; + use ark_pallas::Fr; + + #[test] + fn test_hypercube() { + let expected_results = to_F_dense_matrix(vec![ + vec![0, 0, 0], + vec![1, 0, 0], + vec![0, 1, 0], + vec![1, 1, 0], + vec![0, 0, 1], + vec![1, 0, 1], + vec![0, 1, 1], + vec![1, 1, 1], + ]); + + let bhc = BooleanHypercube::::new(3); + for (i, point) in bhc.clone().enumerate() { + assert_eq!(point, expected_results[i]); + assert_eq!(point, bhc.at_i(i)); + } + } +} diff --git a/src/utils/mle.rs b/src/utils/mle.rs new file mode 100644 index 0000000..b7836f3 --- /dev/null +++ b/src/utils/mle.rs @@ -0,0 +1,167 @@ +/// Some basic MLE utilities +use ark_ff::PrimeField; +use ark_poly::DenseMultilinearExtension; +use ark_std::log2; + +use super::vec::SparseMatrix; + +/// Pad matrix so that its columns and rows are powers of two +pub fn pad_matrix(m: &SparseMatrix) -> SparseMatrix { + let mut r = m.clone(); + r.n_rows = m.n_rows.next_power_of_two(); + r.n_cols = m.n_cols.next_power_of_two(); + r +} + +/// Returns the dense multilinear extension from the given matrix, without modifying the original +/// matrix. +pub fn matrix_to_mle(matrix: SparseMatrix) -> DenseMultilinearExtension { + let n_vars: usize = (log2(matrix.n_rows) + log2(matrix.n_cols)) as usize; // n_vars = s + s' + + // Matrices might need to get padded before turned into an MLE + let padded_matrix = pad_matrix(&matrix); + + // build dense vector representing the sparse padded matrix + let mut v: Vec = vec![F::zero(); padded_matrix.n_rows * padded_matrix.n_cols]; + for (row_i, row) in padded_matrix.coeffs.iter().enumerate() { + for &(value, col_i) in row.iter() { + v[(padded_matrix.n_cols * row_i) + col_i] = value; + } + } + + // convert the dense vector into a mle + vec_to_mle(n_vars, &v) +} + +/// Takes the n_vars and a dense vector and returns its dense MLE. +pub fn vec_to_mle(n_vars: usize, v: &Vec) -> DenseMultilinearExtension { + let v_padded: Vec = if v.len() != (1 << n_vars) { + // pad to 2^n_vars + [ + v.clone(), + std::iter::repeat(F::zero()) + .take((1 << n_vars) - v.len()) + .collect(), + ] + .concat() + } else { + v.clone() + }; + DenseMultilinearExtension::::from_evaluations_vec(n_vars, v_padded) +} + +pub fn dense_vec_to_mle(n_vars: usize, v: &Vec) -> DenseMultilinearExtension { + dbg!(n_vars); + dbg!(v.len()); + // Pad to 2^n_vars + let v_padded: Vec = [ + v.clone(), + std::iter::repeat(F::zero()) + .take((1 << n_vars) - v.len()) + .collect(), + ] + .concat(); + DenseMultilinearExtension::::from_evaluations_vec(n_vars, v_padded) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ccs::tests::get_test_z, + utils::multilinear_polynomial::fix_variables, + utils::multilinear_polynomial::tests::fix_last_variables, + utils::{hypercube::BooleanHypercube, vec::tests::to_F_matrix}, + }; + use ark_poly::MultilinearExtension; + use ark_std::Zero; + + use ark_pallas::Fr; + + #[test] + fn test_matrix_to_mle() { + let A = to_F_matrix::(vec![ + vec![2, 3, 4, 4], + vec![4, 11, 14, 14], + vec![2, 8, 17, 17], + vec![420, 4, 2, 0], + ]); + + let A_mle = matrix_to_mle(A); + dbg!(&A_mle); + assert_eq!(A_mle.evaluations.len(), 16); // 4x4 matrix, thus 2bit x 2bit, thus 2^4=16 evals + + let A = to_F_matrix::(vec![ + vec![2, 3, 4, 4, 1], + vec![4, 11, 14, 14, 2], + vec![2, 8, 17, 17, 3], + vec![420, 4, 2, 0, 4], + vec![420, 4, 2, 0, 5], + ]); + let A_mle = matrix_to_mle(A.clone()); + assert_eq!(A_mle.evaluations.len(), 64); // 5x5 matrix, thus 3bit x 3bit, thus 2^6=64 evals + + // check that the A_mle evaluated over the boolean hypercube equals the matrix A_i_j values + let bhc = BooleanHypercube::new(A_mle.num_vars); + let A_padded = pad_matrix(&A); + let A_padded_dense = A_padded.to_dense(); + for (i, A_row) in A_padded_dense.iter().enumerate() { + for (j, _) in A_row.iter().enumerate() { + let s_i_j = bhc.at_i(i * A_row.len() + j); + assert_eq!(A_mle.evaluate(&s_i_j).unwrap(), A_padded_dense[i][j]); + } + } + } + + #[test] + fn test_vec_to_mle() { + let z = get_test_z::(3); + let n_vars = 3; + let z_mle = dense_vec_to_mle(n_vars, &z); + + // check that the z_mle evaluated over the boolean hypercube equals the vec z_i values + let bhc = BooleanHypercube::new(z_mle.num_vars); + for (i, z_i) in z.iter().enumerate() { + let s_i = bhc.at_i(i); + assert_eq!(z_mle.evaluate(&s_i).unwrap(), z_i.clone()); + } + // for the rest of elements of the boolean hypercube, expect it to evaluate to zero + for i in (z.len())..(1 << z_mle.num_vars) { + let s_i = bhc.at_i(i); + assert_eq!(z_mle.evaluate(&s_i).unwrap(), Fr::zero()); + } + } + + #[test] + fn test_fix_variables() { + let A = to_F_matrix(vec![ + vec![2, 3, 4, 4], + vec![4, 11, 14, 14], + vec![2, 8, 17, 17], + vec![420, 4, 2, 0], + ]); + + let A_mle = matrix_to_mle(A.clone()); + let A = A.to_dense(); + let bhc = BooleanHypercube::new(2); + for (i, y) in bhc.enumerate() { + // First check that the arkworks and espresso funcs match + let expected_fix_left = A_mle.fix_variables(&y); // try arkworks fix_variables + let fix_left = fix_variables(&A_mle, &y); // try espresso fix_variables + assert_eq!(fix_left, expected_fix_left); + + // 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().iter().map(|x| x[i]).collect(); + assert_eq!(fix_left.evaluations, 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[i].clone(); + let fix_right = fix_last_variables(&A_mle, &y); + assert_eq!(fix_right.evaluations, row_i); + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f2c0503..3e564e4 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,5 @@ +pub mod hypercube; +pub mod mle; pub mod vec; // expose espresso local modules diff --git a/src/utils/vec.rs b/src/utils/vec.rs index 5552f69..8e71b32 100644 --- a/src/utils/vec.rs +++ b/src/utils/vec.rs @@ -111,7 +111,10 @@ pub mod tests { use super::*; use ark_pallas::Fr; - pub fn to_F_matrix(M: Vec>) -> Vec> { + pub fn to_F_matrix(M: Vec>) -> SparseMatrix { + dense_matrix_to_sparse(to_F_dense_matrix(M)) + } + pub fn to_F_dense_matrix(M: Vec>) -> Vec> { let mut R: Vec> = vec![Vec::new(); M.len()]; for i in 0..M.len() { R[i] = vec![F::zero(); M[i].len()]; @@ -131,7 +134,7 @@ pub mod tests { #[test] fn test_dense_sparse_conversions() { - let A = to_F_matrix::(vec![ + let A = to_F_dense_matrix::(vec![ vec![0, 1, 0, 0, 0, 0], vec![0, 0, 0, 1, 0, 0], vec![0, 1, 0, 0, 1, 0], @@ -149,7 +152,8 @@ pub mod tests { vec![0, 0, 0, 1, 0, 0], vec![0, 1, 0, 0, 1, 0], vec![5, 0, 0, 0, 0, 1], - ]); + ]) + .to_dense(); let z = to_F_vec(vec![1, 3, 35, 9, 27, 30]); assert_eq!(mat_vec_mul(&A, &z), to_F_vec(vec![3, 9, 30, 35])); assert_eq!( @@ -160,11 +164,11 @@ pub mod tests { let A = to_F_matrix::(vec![vec![2, 3, 4, 5], vec![4, 8, 12, 14], vec![9, 8, 7, 6]]); let v = to_F_vec(vec![19, 55, 50, 3]); - assert_eq!(mat_vec_mul(&A, &v), to_F_vec(vec![418, 1158, 979])); assert_eq!( - mat_vec_mul_sparse(&dense_matrix_to_sparse(A), &v), + mat_vec_mul(&A.to_dense(), &v), to_F_vec(vec![418, 1158, 979]) ); + assert_eq!(mat_vec_mul_sparse(&A, &v), to_F_vec(vec![418, 1158, 979])); } #[test]