Skip to content

Commit

Permalink
Port HyperNova's multifolding from https://github.com/privacy-scaling…
Browse files Browse the repository at this point in the history
…-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 <[email protected]>
  • Loading branch information
arnaucube and asn-d6 committed Aug 25, 2023
1 parent 9565feb commit e15bfe2
Show file tree
Hide file tree
Showing 14 changed files with 1,779 additions and 46 deletions.
13 changes: 9 additions & 4 deletions src/ccs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ impl<C: CurveGroup> CCS<C> {

impl<C: CurveGroup> CCS<C> {
pub fn from_r1cs(r1cs: R1CS<C::ScalarField>, io_len: usize) -> Self {
let m = r1cs.A.n_cols;
let n = r1cs.A.n_rows;
let m = r1cs.A.n_rows;
let n = r1cs.A.n_cols;
CCS {
m,
n,
Expand All @@ -102,15 +102,20 @@ impl<C: CurveGroup> CCS<C> {
}

#[cfg(test)]
mod tests {
pub mod tests {
use super::*;
use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z};
use ark_bls12_377::G1Projective;
use ark_ff::PrimeField;

use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z as r1cs_get_test_z};

pub fn get_test_ccs<C: CurveGroup>() -> CCS<C> {
let r1cs = get_test_r1cs::<C::ScalarField>();
CCS::<C>::from_r1cs(r1cs, 1)
}
pub fn get_test_z<F: PrimeField>(input: usize) -> Vec<F> {
r1cs_get_test_z(input)
}

/// Test that a basic CCS relation can be satisfied
#[test]
Expand Down
33 changes: 7 additions & 26 deletions src/ccs/r1cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,29 @@ impl<F: PrimeField> R1CS<F> {
#[cfg(test)]
pub mod tests {
use super::*;

pub fn to_F_matrix<F: PrimeField>(M: Vec<Vec<usize>>) -> Vec<Vec<F>> {
let mut R: Vec<Vec<F>> = vec![Vec::new(); M.len()];
for i in 0..M.len() {
R[i] = vec![F::zero(); M[i].len()];
for j in 0..M[i].len() {
R[i][j] = F::from(M[i][j] as u64);
}
}
R
}
pub fn to_F_vec<F: PrimeField>(z: Vec<usize>) -> Vec<F> {
let mut r: Vec<F> = vec![F::zero(); z.len()];
for i in 0..z.len() {
r[i] = F::from(z[i] as u64);
}
r
}
use crate::utils::vec::tests::{to_F_matrix, to_F_vec};

pub fn get_test_r1cs<F: PrimeField>() -> R1CS<F> {
// 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::<F>(vec![
let A = to_F_matrix::<F>(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::<F>(vec![
]);
let B = to_F_matrix::<F>(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::<F>(vec![
]);
let C = to_F_matrix::<F>(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::<F> { l: 1, A, B, C }
}
Expand All @@ -72,8 +55,6 @@ pub mod tests {
input * input, // x^2
input * input * input, // x^2 * x
input * input * input + input, // x^3 + x
0, // pad to pow of 2
0,
])
}
}
238 changes: 238 additions & 0 deletions src/folding/hypernova/cccs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
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 crate::ccs::CCS;
use crate::folding::hypernova::utils::compute_sum_Mz;
use crate::Error;

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;

/// 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<F: PrimeField> {
pub w: Vec<F>,
pub r_w: F, // randomness used in the Pedersen commitment of w
}

/// Committed CCS instance
#[derive(Debug, Clone)]
pub struct CCCS<C: CurveGroup> {
// Underlying CCS structure
pub ccs: CCS<C>,

// Commitment to witness
pub C: C,
// Public input/output
pub x: Vec<C::ScalarField>,
}

impl<C: CurveGroup> CCS<C> {
pub fn to_cccs<R: Rng>(
&self,
rng: &mut R,
pedersen_params: &PedersenParams<C>,
z: &[C::ScalarField],
) -> (CCCS<C>, Witness<C::ScalarField>) {
let w: Vec<C::ScalarField> = z[(1 + self.l)..].to_vec();
let r_w = C::ScalarField::rand(rng);
let C = Pedersen::<C>::commit(pedersen_params, &w, &r_w);

(
CCCS::<C> {
ccs: self.clone(),
C,
x: z[1..(1 + self.l)].to_vec(),
},
Witness::<C::ScalarField> { w, r_w },
)
}
}

impl<C: CurveGroup> CCCS<C> {
/// 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<C::ScalarField>) -> VirtualPolynomial<C::ScalarField> {
let z_mle = vec_to_mle(self.ccs.s_prime, z);
let mut q = VirtualPolynomial::<C::ScalarField>::new(self.ccs.s);

for i in 0..self.ccs.q {
let mut prod: VirtualPolynomial<C::ScalarField> =
VirtualPolynomial::<C::ScalarField>::new(self.ccs.s);
for j in self.ccs.S[i].clone() {
let M_j = matrix_to_mle(self.ccs.M[j].clone());

let sum_Mz = compute_sum_Mz(M_j, &z_mle, self.ccs.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.ccs.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<C::ScalarField>,
beta: &[C::ScalarField],
) -> VirtualPolynomial<C::ScalarField> {
let q = self.compute_q(z);
q.build_f_hat(beta).unwrap()
}

/// Perform the check of the CCCS instance described at section 4.1
pub fn check_relation(
&self,
pedersen_params: &PedersenParams<C>,
w: &Witness<C::ScalarField>,
) -> 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<C::ScalarField> =
[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 = self.compute_q(&z);
for x in BooleanHypercube::new(self.ccs.s) {
if !q_x.evaluate(&x).unwrap().is_zero() {
return Err(Error::NotSatisfied);
}
}

Ok(())
}
}

#[cfg(test)]
pub mod test {
use super::*;
use crate::ccs::tests::{get_test_ccs, get_test_z};
use ark_std::test_rng;
use ark_std::UniformRand;

use ark_bls12_377::{Fr, G1Projective};

/// 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::<G1Projective>();
let z = get_test_z(3);

let pedersen_params = Pedersen::<G1Projective>::new_params(&mut rng, ccs.n - ccs.l - 1);
let (cccs, _) = ccs.to_cccs(&mut rng, &pedersen_params, &z);
let q = cccs.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<Fr> = (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 = get_test_ccs();
let z = get_test_z(3);
ccs.check_relation(&z).unwrap();

let pedersen_params = Pedersen::<G1Projective>::new_params(&mut rng, ccs.n - ccs.l - 1);
let (cccs, _) = ccs.to_cccs(&mut rng, &pedersen_params, &z);

let beta: Vec<Fr> = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect();

// Compute Q(x) = eq(beta, x) * q(x).
let Q = cccs.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 = get_test_ccs();
let z = get_test_z(3);
ccs.check_relation(&z).unwrap();

let pedersen_params = Pedersen::<G1Projective>::new_params(&mut rng, ccs.n - ccs.l - 1);
let (cccs, _) = ccs.to_cccs(&mut rng, &pedersen_params, &z);

// 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(&z);
for d in BooleanHypercube::new(ccs.s) {
let Q_at_d = cccs.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<Fr> = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect();
let Q_at_r = cccs.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());
}
}
Loading

0 comments on commit e15bfe2

Please sign in to comment.