diff --git a/Cargo.toml b/Cargo.toml index dde7c9ac..0b6dc9fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,10 @@ tracing = { version = "0.1", default-features = false, features = [ "attributes" tracing-subscriber = { version = "0.2" } [features] -default = ["parallel", "nova", "hypernova"] -hypernova=[] +default = ["parallel", "nova", "hypernova", "protogalaxy"] nova=[] +hypernova=[] +protogalaxy=[] parallel = [ "ark-std/parallel", diff --git a/src/folding/mod.rs b/src/folding/mod.rs index 7bafdf53..89adfa44 100644 --- a/src/folding/mod.rs +++ b/src/folding/mod.rs @@ -3,3 +3,5 @@ pub mod circuits; pub mod hypernova; #[cfg(feature = "nova")] pub mod nova; +#[cfg(feature = "protogalaxy")] +pub mod protogalaxy; diff --git a/src/folding/protogalaxy/mod.rs b/src/folding/protogalaxy/mod.rs new file mode 100644 index 00000000..687fb177 --- /dev/null +++ b/src/folding/protogalaxy/mod.rs @@ -0,0 +1,600 @@ +use ark_crypto_primitives::sponge::Absorb; +use ark_ec::{CurveGroup, Group}; +use ark_ff::PrimeField; +use ark_poly::{ + univariate::{DensePolynomial, SparsePolynomial}, + DenseUVPolynomial, EvaluationDomain, Evaluations, GeneralEvaluationDomain, Polynomial, +}; +use ark_std::log2; +use ark_std::{cfg_into_iter, Zero}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use std::marker::PhantomData; +use std::ops::Add; + +pub mod traits; +pub mod utils; +use traits::ProtoGalaxyTranscript; +use utils::{all_powers, bit_decompose, powers_of_beta}; + +use crate::ccs::r1cs::R1CS; +use crate::transcript::Transcript; +use crate::utils::vec::*; +use crate::{unwrap_or_return_err, Error}; + +#[derive(Clone, Debug)] +pub struct CommittedInstance { + phi: C, + betas: Vec, + e: C::ScalarField, +} + +#[derive(Clone, Debug)] +pub struct Witness { + w: Vec, + r_w: C::ScalarField, +} + +#[derive(Clone, Debug)] +pub struct Folding { + _phantom: PhantomData, +} +impl Folding +where + ::ScalarField: Absorb, + ::BaseField: Absorb, +{ + #![allow(clippy::type_complexity)] + pub fn prove( + transcript: &mut (impl Transcript + ProtoGalaxyTranscript), + r1cs: &R1CS, + // running instance + instance: &CommittedInstance, + w: &Witness, + // incomming instances + vec_instances: &[CommittedInstance], + vec_w: &[Witness], + ) -> Result< + ( + CommittedInstance, + Witness, + Vec, + Vec, + ), + Error, + > { + let t = instance.betas.len(); + let n = r1cs.A.n_cols; + assert_eq!(w.w.len(), n); + assert_eq!(log2(n) as usize, t); + + // absorb the committed instances + transcript.absorb_committed_instance(instance)?; + for ci in vec_instances.iter() { + transcript.absorb_committed_instance(ci)?; + } + + let delta = transcript.get_challenge(); + let deltas = powers_of_beta(delta, t); + + let f_w = eval_f(r1cs, &w.w)?; + + // F(X) + let mut F_X: SparsePolynomial = SparsePolynomial::zero(); + for (i, f_w_i) in f_w.iter().enumerate() { + let lhs = pow_i_over_x::(i, &instance.betas, &deltas); + let curr = &lhs * *f_w_i; + F_X = F_X.add(curr); + } + + let F_X_dense = DensePolynomial::from(F_X.clone()); + transcript.absorb_vec(&F_X_dense.coeffs); + + let alpha = transcript.get_challenge(); + + // eval F(alpha) + let F_alpha = F_X.evaluate(&alpha); + + // betas* + let betas_star: Vec = instance + .betas + .iter() + .zip( + deltas + .iter() + .map(|delta_i| alpha * delta_i) + .collect::>(), + ) + .map(|(beta_i, delta_i_alpha)| *beta_i + delta_i_alpha) + .collect(); + + // sanity check: check that the new randomized instnace (the original instance but with + // 'refreshed' randomness) satisfies the relation. + check_instance( + r1cs, + &CommittedInstance { + phi: instance.phi, + betas: betas_star.clone(), + e: F_alpha, + }, + w, + )?; + + let mut ws: Vec> = Vec::new(); + ws.push(w.w.clone()); + for wj in vec_w.iter() { + assert_eq!(wj.w.len(), n); + ws.push(wj.w.clone()); + } + + let k = vec_instances.len(); + let H = unwrap_or_return_err!( + GeneralEvaluationDomain::::new(k + 1), + Err(Error::NewDomainFail) + ); + // WIP review t/d + let EH = unwrap_or_return_err!( + GeneralEvaluationDomain::::new(t * k + 1), + Err(Error::NewDomainFail) + ); + let L_X: Vec> = lagrange_polys(H); + + // K(X) computation in a naive way, next iterations will compute K(X) as described in Claim + // 4.5 of the paper. + let mut G_evals: Vec = vec![C::ScalarField::zero(); EH.size()]; + for (hi, h) in EH.elements().enumerate() { + // each iteration evaluates G(h) + // inner = L_0(x) * w + \sum_k L_i(x) * w_j + let mut inner: Vec = vec![C::ScalarField::zero(); ws[0].len()]; + for (i, w) in ws.iter().enumerate() { + // Li_w = Li(X) * wj + let mut Li_w: Vec> = + vec![DensePolynomial::::zero(); w.len()]; + for (j, wj) in w.iter().enumerate() { + let Li_wj = &L_X[i] * *wj; + Li_w[j] = Li_wj; + } + // Li_w_h = Li_w(h) = Li(h) * wj + let mut Liw_h: Vec = vec![C::ScalarField::zero(); w.len()]; + for (j, _) in Li_w.iter().enumerate() { + Liw_h[j] = Li_w[j].evaluate(&h); + } + + for j in 0..inner.len() { + inner[j] += Liw_h[j]; + } + } + let f_ev = eval_f(r1cs, &inner)?; + + let mut Gsum = C::ScalarField::zero(); + for (i, f_ev_i) in f_ev.iter().enumerate() { + let pow_i_betas = pow_i(i, &betas_star); + let curr = pow_i_betas * f_ev_i; + Gsum += curr; + } + G_evals[hi] = Gsum; + } + let G_X: DensePolynomial = + Evaluations::::from_vec_and_domain(G_evals.clone(), EH).interpolate(); + let Z_X: DensePolynomial = H.vanishing_polynomial().into(); + // K(X) = (G(X)- F(alpha)*L_0(X)) / Z(X) + let L0_e = &L_X[0] * F_alpha; // L0(X)*F(a) will be 0 in the native case + let G_L0e = &G_X - &L0_e; + // TODO move division by Z_X to the prev loop + let (K_X, remainder) = unwrap_or_return_err!( + G_L0e.divide_by_vanishing_poly(H), + // using Error::ShouldNotReach because the method divide_by_vanishing_poly should never + // return 'None' despite returning an 'Option'. + Err(Error::ShouldNotReach) + ); + assert!(remainder.is_zero()); // sanity check + + transcript.absorb_vec(&K_X.coeffs); + + let gamma = transcript.get_challenge(); + + let e_star = + F_alpha * L_X[0].evaluate(&gamma) + Z_X.evaluate(&gamma) * K_X.evaluate(&gamma); + + let mut phi_star: C = instance.phi * L_X[0].evaluate(&gamma); + for i in 0..k { + phi_star += vec_instances[i].phi * L_X[i + 1].evaluate(&gamma); + } + let mut w_star: Vec = vec_scalar_mul(&w.w, &L_X[0].evaluate(&gamma)); + for i in 0..k { + w_star = vec_add( + &w_star, + &vec_scalar_mul(&vec_w[i].w, &L_X[i + 1].evaluate(&gamma)), + )?; + } + let mut r_w_star: C::ScalarField = w.r_w * L_X[0].evaluate(&gamma); + for i in 0..k { + r_w_star += vec_w[i].r_w * L_X[i + 1].evaluate(&gamma); + } + + Ok(( + CommittedInstance { + betas: betas_star, + phi: phi_star, + e: e_star, + }, + Witness { + w: w_star, + r_w: r_w_star, + }, + F_X_dense.coeffs, + K_X.coeffs, + )) + } + + pub fn verify( + transcript: &mut (impl Transcript + ProtoGalaxyTranscript), + r1cs: &R1CS, + // running instance + instance: &CommittedInstance, + // incomming instances + vec_instances: &[CommittedInstance], + // polys from P + F_coeffs: Vec, + K_coeffs: Vec, + ) -> Result, Error> { + let t = instance.betas.len(); + let n = r1cs.A.n_cols; + + // absorb the committed instances + transcript.absorb_committed_instance(instance)?; + for ci in vec_instances.iter() { + transcript.absorb_committed_instance(ci)?; + } + + let delta = transcript.get_challenge(); + let deltas = powers_of_beta(delta, t); + + transcript.absorb_vec(&F_coeffs); + + let alpha = transcript.get_challenge(); + let alphas = all_powers(alpha, n); + + // F(alpha) = e + \sum_t F_i * alpha^i + let mut F_alpha = instance.e; + for (i, F_i) in F_coeffs.iter().skip(1).enumerate() { + F_alpha += *F_i * alphas[i + 1]; + } + + let betas_star: Vec = instance + .betas + .iter() + .zip( + deltas + .iter() + .map(|delta_i| alpha * delta_i) + .collect::>(), + ) + .map(|(beta_i, delta_i_alpha)| *beta_i + delta_i_alpha) + .collect(); + + let k = vec_instances.len(); + let H = unwrap_or_return_err!( + GeneralEvaluationDomain::::new(k + 1), + Err(Error::NewDomainFail) + ); + let L_X: Vec> = lagrange_polys(H); + let Z_X: DensePolynomial = H.vanishing_polynomial().into(); + let K_X: DensePolynomial = + DensePolynomial::::from_coefficients_vec(K_coeffs); + + transcript.absorb_vec(&K_X.coeffs); + + let gamma = transcript.get_challenge(); + + let e_star = + F_alpha * L_X[0].evaluate(&gamma) + Z_X.evaluate(&gamma) * K_X.evaluate(&gamma); + + let mut phi_star: C = instance.phi * L_X[0].evaluate(&gamma); + for i in 0..k { + phi_star += vec_instances[i].phi * L_X[i + 1].evaluate(&gamma); + } + + // return the folded instance + Ok(CommittedInstance { + betas: betas_star, + phi: phi_star, + e: e_star, + }) + } +} + +// naive impl of pow_i for betas, assuming that betas=(b, b^2, b^4, ..., b^{2^{t-1}}) +fn pow_i(i: usize, betas: &Vec) -> F { + // WIP check if makes more sense to do it with ifs instead of arithmetic + + let n = 2_u64.pow(betas.len() as u32); + let b = bit_decompose(i as u64, n as usize); + + let mut r: F = F::one(); + for (j, beta_j) in betas.iter().enumerate() { + let mut b_j = F::zero(); + if b[j] { + b_j = F::one(); + } + r *= (F::one() - b_j) + b_j * beta_j; + } + r +} + +fn pow_i_over_x(i: usize, betas: &Vec, deltas: &Vec) -> SparsePolynomial { + assert_eq!(betas.len(), deltas.len()); + + let n = 2_u64.pow(betas.len() as u32); + let b = bit_decompose(i as u64, n as usize); + + let mut r: SparsePolynomial = + SparsePolynomial::::from_coefficients_vec(vec![(0, F::one())]); // start with r(x) = 1 + for (j, beta_j) in betas.iter().enumerate() { + if b[j] { + let curr: SparsePolynomial = + SparsePolynomial::::from_coefficients_vec(vec![(0, *beta_j), (1, deltas[j])]); + r = r.mul(&curr); + } + } + r +} + +// lagrange_polys method from caulk: https://github.com/caulk-crypto/caulk/tree/8210b51fb8a9eef4335505d1695c44ddc7bf8170/src/multi/setup.rs#L300 +fn lagrange_polys(domain_n: GeneralEvaluationDomain) -> Vec> { + let mut lagrange_polynomials: Vec> = Vec::new(); + for i in 0..domain_n.size() { + let evals: Vec = cfg_into_iter!(0..domain_n.size()) + .map(|k| if k == i { F::one() } else { F::zero() }) + .collect(); + lagrange_polynomials.push(Evaluations::from_vec_and_domain(evals, domain_n).interpolate()); + } + lagrange_polynomials +} + +// f(w) in R1CS context. For the moment we use R1CS, in the future we will abstract this with a +// trait +fn eval_f(r1cs: &R1CS, w: &[F]) -> Result, Error> { + let Az = mat_vec_mul_sparse(&r1cs.A, w); + let Bz = mat_vec_mul_sparse(&r1cs.B, w); + let Cz = mat_vec_mul_sparse(&r1cs.C, w); + let AzBz = hadamard(&Az, &Bz)?; + vec_sub(&AzBz, &Cz) +} + +fn check_instance( + r1cs: &R1CS, + instance: &CommittedInstance, + w: &Witness, +) -> Result<(), Error> { + assert_eq!(instance.betas.len(), log2(w.w.len()) as usize); + + let f_w = eval_f(r1cs, &w.w)?; // f(w) + + let mut r = C::ScalarField::zero(); + for (i, f_w_i) in f_w.iter().enumerate() { + r += pow_i(i, &instance.betas) * f_w_i; + } + if instance.e == r { + return Ok(()); + } + Err(Error::NotSatisfied) +} + +#[cfg(test)] +mod tests { + use super::*; + use ark_pallas::{Fr, Projective}; + use ark_std::UniformRand; + + use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z}; + use crate::pedersen::Pedersen; + use crate::transcript::poseidon::{tests::poseidon_test_config, PoseidonTranscript}; + + #[test] + fn test_pow_i() { + let mut rng = ark_std::test_rng(); + let t = 4; + let n = 16; + let beta = Fr::rand(&mut rng); + let betas = powers_of_beta(beta, t); + let not_betas = all_powers(beta, n); + + #[allow(clippy::needless_range_loop)] + for i in 0..n { + assert_eq!(pow_i(i, &betas), not_betas[i]); + } + } + + #[test] + fn test_pow_i_over_x() { + let mut rng = ark_std::test_rng(); + let t = 3; + let n = 8; + let beta = Fr::rand(&mut rng); + let delta = Fr::rand(&mut rng); + let betas = powers_of_beta(beta, t); + let deltas = powers_of_beta(delta, t); + + // compute b + X*d, with X=rand + let x = Fr::rand(&mut rng); + let bxd = vec_add(&betas, &vec_scalar_mul(&deltas, &x)).unwrap(); + + // assert that computing pow_over_x of betas,deltas, is equivalent to first computing the + // vector [betas+X*deltas] and then computing pow_i over it + for i in 0..n { + let pow_i1 = pow_i_over_x(i, &betas, &deltas); + let pow_i2 = pow_i(i, &bxd); + assert_eq!(pow_i1.evaluate(&x), pow_i2); + } + } + + #[test] + fn test_eval_f() { + let r1cs = get_test_r1cs::(); + let mut z = get_test_z::(3); + + let f_w = eval_f(&r1cs, &z).unwrap(); + assert!(is_zero_vec(&f_w)); + + z[1] = Fr::from(111); + let f_w = eval_f(&r1cs, &z).unwrap(); + assert!(!is_zero_vec(&f_w)); + } + + // k represents the number of instances to be fold, appart from the running instance + #[allow(clippy::type_complexity)] + fn prepare_inputs( + k: usize, + ) -> ( + Witness, + CommittedInstance, + Vec>, + Vec>, + ) { + let mut rng = ark_std::test_rng(); + let pedersen_params = Pedersen::::new_params(&mut rng, 100); // 100 is wip, will get it from actual vec + + let z = get_test_z::(3); + let mut zs: Vec> = Vec::new(); + for i in 0..k { + let z_i = get_test_z::(i + 4); + zs.push(z_i); + } + + let n = z.len(); + let t = log2(n) as usize; + + let beta = Fr::rand(&mut rng); + let betas = powers_of_beta(beta, t); + + let witness = Witness:: { + w: z.clone(), + r_w: Fr::rand(&mut rng), + }; + let phi = + Pedersen::::commit(&pedersen_params, &witness.w, &witness.r_w).unwrap(); + let instance = CommittedInstance:: { + phi, + betas: betas.clone(), + e: Fr::zero(), + }; + // same for the other instances + let mut witnesses: Vec> = Vec::new(); + let mut instances: Vec> = Vec::new(); + #[allow(clippy::needless_range_loop)] + for i in 0..k { + let witness_i = Witness:: { + w: zs[i].clone(), + r_w: Fr::rand(&mut rng), + }; + let phi_i = + Pedersen::::commit(&pedersen_params, &witness_i.w, &witness_i.r_w) + .unwrap(); + let instance_i = CommittedInstance:: { + phi: phi_i, + betas: betas.clone(), + e: Fr::zero(), + }; + witnesses.push(witness_i); + instances.push(instance_i); + } + + (witness, instance, witnesses, instances) + } + + #[test] + fn test_fold_native_case() { + let k = 6; + let (witness, instance, witnesses, instances) = prepare_inputs(k); + let r1cs = get_test_r1cs::(); + + // init Prover & Verifier's transcript + let poseidon_config = poseidon_test_config::(); + let mut transcript_p = PoseidonTranscript::::new(&poseidon_config); + let mut transcript_v = PoseidonTranscript::::new(&poseidon_config); + + let (folded_instance, folded_witness, F_coeffs, K_coeffs) = Folding::::prove( + &mut transcript_p, + &r1cs, + &instance, + &witness, + &instances, + &witnesses, + ) + .unwrap(); + + // veriier + let folded_instance_v = Folding::::verify( + &mut transcript_v, + &r1cs, + &instance, + &instances, + F_coeffs, + K_coeffs, + ) + .unwrap(); + + // check that prover & verifier folded instances are the same values + assert_eq!(folded_instance.phi, folded_instance_v.phi); + assert_eq!(folded_instance.betas, folded_instance_v.betas); + assert_eq!(folded_instance.e, folded_instance_v.e); + assert!(!folded_instance.e.is_zero()); + + // check that the folded instance satisfies the relation + check_instance(&r1cs, &folded_instance, &folded_witness).unwrap(); + } + + #[test] + fn test_fold_various_iterations() { + let r1cs = get_test_r1cs::(); + + // init Prover & Verifier's transcript + let poseidon_config = poseidon_test_config::(); + let mut transcript_p = PoseidonTranscript::::new(&poseidon_config); + let mut transcript_v = PoseidonTranscript::::new(&poseidon_config); + + let (mut running_witness, mut running_instance, _, _) = prepare_inputs(0); + + // fold k instances on each of num_iters iterations + let k = 6; + let num_iters = 10; + for _ in 0..num_iters { + // generate the instances to be fold + let (_, _, witnesses, instances) = prepare_inputs(k); + + let (folded_instance, folded_witness, F_coeffs, K_coeffs) = + Folding::::prove( + &mut transcript_p, + &r1cs, + &running_instance, + &running_witness, + &instances, + &witnesses, + ) + .unwrap(); + + // veriier + let folded_instance_v = Folding::::verify( + &mut transcript_v, + &r1cs, + &running_instance, + &instances, + F_coeffs, + K_coeffs, + ) + .unwrap(); + + // check that prover & verifier folded instances are the same values + assert_eq!(folded_instance.phi, folded_instance_v.phi); + assert_eq!(folded_instance.betas, folded_instance_v.betas); + assert_eq!(folded_instance.e, folded_instance_v.e); + assert!(!folded_instance.e.is_zero()); + + // check that the folded instance satisfies the relation + check_instance(&r1cs, &folded_instance, &folded_witness).unwrap(); + + running_witness = folded_witness; + running_instance = folded_instance; + } + } +} diff --git a/src/folding/protogalaxy/traits.rs b/src/folding/protogalaxy/traits.rs new file mode 100644 index 00000000..9fea6915 --- /dev/null +++ b/src/folding/protogalaxy/traits.rs @@ -0,0 +1,23 @@ +use ark_crypto_primitives::sponge::Absorb; +use ark_ec::{CurveGroup, Group}; + +use super::CommittedInstance; +use crate::transcript::{poseidon::PoseidonTranscript, Transcript}; +use crate::Error; + +/// ProtoGalaxyTranscript extends Transcript with the method to absorb ProtoGalaxy's +/// CommittedInstance. +pub trait ProtoGalaxyTranscript: Transcript { + fn absorb_committed_instance(&mut self, ci: &CommittedInstance) -> Result<(), Error> { + self.absorb_point(&ci.phi)?; + self.absorb_vec(&ci.betas); + self.absorb(&ci.e); + Ok(()) + } +} + +// implements ProtoGalaxyTranscript for PoseidonTranscript +impl ProtoGalaxyTranscript for PoseidonTranscript where + ::ScalarField: Absorb +{ +} diff --git a/src/folding/protogalaxy/utils.rs b/src/folding/protogalaxy/utils.rs new file mode 100644 index 00000000..ac558304 --- /dev/null +++ b/src/folding/protogalaxy/utils.rs @@ -0,0 +1,29 @@ +use ark_ff::PrimeField; + +// returns (b, b^2, b^4, ..., b^{2^{t-1}}) +pub fn powers_of_beta(b: F, t: usize) -> Vec { + let mut r = vec![F::zero(); t]; + r[0] = b; + for i in 1..t { + r[i] = r[i - 1].square(); + } + r +} +pub fn all_powers(a: F, n: usize) -> Vec { + let mut r = vec![F::zero(); n]; + // TODO more efficiently + for (i, r_i) in r.iter_mut().enumerate() { + *r_i = a.pow([i as u64]); + } + r +} + +pub fn bit_decompose(input: u64, n: usize) -> Vec { + let mut res = Vec::with_capacity(n); + let mut i = input; + for _ in 0..n { + res.push(i & 1 == 1); + i >>= 1; + } + res +} diff --git a/src/lib.rs b/src/lib.rs index 8e8a817c..d4206fae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,12 @@ pub enum Error { R1CSUnrelaxedFail, #[error("Could not find the inner ConstraintSystem")] NoInnerConstraintSystem, + #[error("Could not construct the Evaluation Domain")] + NewDomainFail, + #[error( + "This error should not be reached. If you found this error, please open an issue at https://github.com/privacy-scaling-explorations/folding-schemes/issues/new describing the case in which you reached it. Thank you very much." + )] + ShouldNotReach, } #[macro_export]