From af289aaad0b1477c75d0d643d851139eb76e07b5 Mon Sep 17 00:00:00 2001 From: porcuquine Date: Tue, 24 Oct 2023 15:38:54 -0700 Subject: [PATCH] Port paranova. --- src/circuit.rs | 18 +- src/errors.rs | 3 + src/gadgets/r1cs.rs | 105 ++++- src/lib.rs | 111 ++++- src/nifs.rs | 80 ++-- src/parallel_circuit.rs | 586 +++++++++++++++++++++++++++ src/parallel_prover.rs | 847 +++++++++++++++++++++++++++++++++++++++ src/r1cs/mod.rs | 32 +- src/supernova/circuit.rs | 6 +- src/supernova/mod.rs | 53 ++- src/supernova/test.rs | 2 + src/traits/circuit.rs | 12 + 12 files changed, 1789 insertions(+), 66 deletions(-) create mode 100644 src/parallel_circuit.rs create mode 100644 src/parallel_prover.rs diff --git a/src/circuit.rs b/src/circuit.rs index 7307e1359..a9bbbe7a9 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -13,7 +13,7 @@ use crate::{ alloc_num_equals, alloc_scalar_as_base, alloc_zero, conditionally_select_vec, le_bits_to_num, }, }, - r1cs::{R1CSInstance, RelaxedR1CSInstance}, + r1cs::RelaxedR1CSInstance, traits::{ circuit::StepCircuit, commitment::CommitmentTrait, Group, ROCircuitTrait, ROConstantsCircuit, }, @@ -31,9 +31,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Abomonation)] pub struct NovaAugmentedCircuitParams { - limb_width: usize, - n_limbs: usize, - is_primary_circuit: bool, // A boolean indicating if this is the primary circuit + pub(crate) limb_width: usize, + pub(crate) n_limbs: usize, + pub(crate) is_primary_circuit: bool, // A boolean indicating if this is the primary circuit } impl NovaAugmentedCircuitParams { @@ -54,7 +54,7 @@ pub struct NovaAugmentedCircuitInputs { z0: Vec, zi: Option>, U: Option>, - u: Option>, + u: Option>, T: Option>, } @@ -66,7 +66,7 @@ impl NovaAugmentedCircuitInputs { z0: Vec, zi: Option>, U: Option>, - u: Option>, + u: Option>, T: Option>, ) -> Self { Self { @@ -372,6 +372,7 @@ mod tests { bellpepper::r1cs::{NovaShape, NovaWitness}, gadgets::utils::scalar_as_base, provider::poseidon::PoseidonConstantsCircuit, + r1cs::RelaxedR1CSWitness, traits::circuit::TrivialCircuit, }; @@ -421,8 +422,11 @@ mod tests { NovaAugmentedCircuit::new(primary_params, Some(inputs1), &tc1, ro_consts1); let _ = circuit1.synthesize(&mut cs1); let (inst1, witness1) = cs1.r1cs_instance_and_witness(&shape1, &ck1).unwrap(); + + let inst1 = RelaxedR1CSInstance::from_r1cs_instance(&ck1, &shape1, &inst1); + let witness1 = RelaxedR1CSWitness::from_r1cs_witness(&shape1, &witness1); // Make sure that this is satisfiable - assert!(shape1.is_sat(&ck1, &inst1, &witness1).is_ok()); + assert!(shape1.is_sat_relaxed(&ck1, &inst1, &witness1).is_ok()); // Execute the base case for the secondary let zero2 = <::Base as Field>::ZERO; diff --git a/src/errors.rs b/src/errors.rs index 6474cfb51..b3d55ebce 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -50,6 +50,9 @@ pub enum NovaError { /// returned when the multiset check fails #[error("InvalidMultisetProof")] InvalidMultisetProof, + /// returned when the tree node is attempting to merge with a node which has a greater than 1 gap in steps + #[error("InvalidNodeMerge")] + InvalidNodeMerge, /// returned when the product proof check fails #[error("InvalidProductProof")] InvalidProductProof, diff --git a/src/gadgets/r1cs.rs b/src/gadgets/r1cs.rs index 827bd7d5e..f6d651888 100644 --- a/src/gadgets/r1cs.rs +++ b/src/gadgets/r1cs.rs @@ -12,7 +12,7 @@ use crate::{ conditionally_select_bignat, le_bits_to_num, }, }, - r1cs::{R1CSInstance, RelaxedR1CSInstance}, + r1cs::RelaxedR1CSInstance, traits::{commitment::CommitmentTrait, Group, ROCircuitTrait, ROConstantsCircuit}, }; use bellpepper::gadgets::{boolean::Boolean, num::AllocatedNum, Assignment}; @@ -31,7 +31,7 @@ impl AllocatedR1CSInstance { /// Takes the r1cs instance and creates a new allocated r1cs instance pub fn alloc::Base>>( mut cs: CS, - u: Option<&R1CSInstance>, + u: Option<&RelaxedR1CSInstance>, ) -> Result { // Check that the incoming instance has exactly 2 io let W = AllocatedPoint::alloc( @@ -315,6 +315,107 @@ impl AllocatedRelaxedR1CSInstance { }) } + /// Folds self with a relaxed r1cs instance and returns the result + #[allow(clippy::too_many_arguments)] + #[allow(unused)] + pub fn fold_with_relaxed_r1cs::Base>>( + &self, + mut cs: CS, + params: AllocatedNum, // hash of R1CSShape of F' + u: AllocatedRelaxedR1CSInstance, + T: AllocatedPoint, + ro_consts: ROConstantsCircuit, + limb_width: usize, + n_limbs: usize, + ) -> Result, SynthesisError> { + // Compute r: + let mut ro = G::ROCircuit::new(ro_consts, NUM_FE_FOR_RO + 10); + ro.absorb(¶ms); + self.absorb_in_ro(cs.namespace(|| "absorb running instance"), &mut ro)?; + u.absorb_in_ro(cs.namespace(|| "absorb running instance u"), &mut ro)?; + ro.absorb(&T.x); + ro.absorb(&T.y); + ro.absorb(&T.is_infinity); + let r_bits = ro.squeeze(cs.namespace(|| "r bits"), NUM_CHALLENGE_BITS)?; + let r = le_bits_to_num(cs.namespace(|| "r"), &r_bits)?; + + // W_fold = self.W + r * u.W + let rW = u.W.scalar_mul(cs.namespace(|| "r * u.W"), &r_bits)?; + let W_fold = self.W.add(cs.namespace(|| "self.W + r * u.W"), &rW)?; + + // E_fold = self.E + r * T + r * r * U.E + let rT = T.scalar_mul(cs.namespace(|| "r * T"), &r_bits)?; + let r_e_2 = u.E.scalar_mul(cs.namespace(|| "r * E_2"), &r_bits)?; + // Todo - there has to be a better way than 2 scalar mul + let r_squared_e_2 = r_e_2.scalar_mul(cs.namespace(|| "r * r * E_2"), &r_bits)?; + let rT_plus_r_squared_E_2 = rT.add(cs.namespace(|| "rT + r * r * E_2"), &r_squared_e_2)?; + let E_fold = self + .E + .add(cs.namespace(|| "self.E + r * T"), &rT_plus_r_squared_E_2)?; + + // u_fold = u_r + r + let u_u_r = AllocatedNum::alloc(cs.namespace(|| "u_u times r"), || { + Ok(*self.u.get_value().get()? * r.get_value().get()?) + })?; + let u_fold = AllocatedNum::alloc(cs.namespace(|| "u_fold"), || { + Ok(*self.u.get_value().get()? + u_u_r.get_value().get()?) + })?; + cs.enforce( + || "Check u_fold", + |lc| lc, + |lc| lc, + |lc| lc + u_fold.get_variable() - self.u.get_variable() - u_u_r.get_variable(), + ); + + // Fold the IO: + // Analyze r into limbs + let r_bn = BigNat::from_num( + cs.namespace(|| "allocate r_bn"), + &Num::from(r), + limb_width, + n_limbs, + )?; + + // Allocate the order of the non-native field as a constant + let m_bn = alloc_bignat_constant( + cs.namespace(|| "alloc m"), + &G::get_curve_params().2, + limb_width, + n_limbs, + )?; + + // Analyze X0 to bignat, NOTE - we copied this code from above but here changed it because the u.X0 is already BigNat + // for u of the type relaxed R1CS + let X0_bn = u.X0.clone(); + + // Fold self.X[0] + r * X[0] + let (_, r_0) = X0_bn.mult_mod(cs.namespace(|| "r*X[0]"), &r_bn, &m_bn)?; + // add X_r[0] + let r_new_0 = self.X0.add(&r_0)?; + // Now reduce + let X0_fold = r_new_0.red_mod(cs.namespace(|| "reduce folded X[0]"), &m_bn)?; + + // Analyze X1 to bignat, NOTE - we copied this code from above but here changed it because the u.X0 is already BigNat + // for u of the type relaxed R1CS + let X1_bn = u.X1.clone(); + + // Fold self.X[1] + r * X[1] + let (_, r_1) = X1_bn.mult_mod(cs.namespace(|| "r*X[1]"), &r_bn, &m_bn)?; + // add X_r[1] + let r_new_1 = self.X1.add(&r_1)?; + + // Now reduce + let X1_fold = r_new_1.red_mod(cs.namespace(|| "reduce folded X[1]"), &m_bn)?; + + Ok(Self { + W: W_fold, + E: E_fold, + u: u_fold, + X0: X0_fold, + X1: X1_fold, + }) + } + /// If the condition is true then returns this otherwise it returns the other pub fn conditionally_select::Base>>( &self, diff --git a/src/lib.rs b/src/lib.rs index eef3ec2a0..478eb876d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,11 +15,13 @@ mod bellpepper; mod circuit; mod digest; mod nifs; +mod parallel_circuit; // public modules pub mod constants; pub mod errors; pub mod gadgets; +pub mod parallel_prover; pub mod provider; pub mod r1cs; pub mod spartan; @@ -337,7 +339,11 @@ where z0_secondary, None, None, - Some(u_primary.clone()), + Some(RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_primary, + &pp.circuit_shape_primary.r1cs_shape, + &u_primary, + )), None, ); let circuit_secondary: NovaAugmentedCircuit<'_, G1, C2> = NovaAugmentedCircuit::new( @@ -434,8 +440,16 @@ where &pp.circuit_shape_secondary.r1cs_shape, &self.r_U_secondary, &self.r_W_secondary, - &self.l_u_secondary, - &self.l_w_secondary, + &RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_secondary, + &pp.circuit_shape_secondary.r1cs_shape, + &self.l_u_secondary, + ), + &RelaxedR1CSWitness::from_r1cs_witness( + &pp.circuit_shape_secondary.r1cs_shape, + &self.l_w_secondary, + ), + false, ) .expect("Unable to fold secondary"); @@ -446,7 +460,11 @@ where z0_primary, Some(self.zi_primary.clone()), Some(self.r_U_secondary.clone()), - Some(self.l_u_secondary.clone()), + Some(RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_secondary, + &pp.circuit_shape_secondary.r1cs_shape, + &self.l_u_secondary, + )), Some(Commitment::::decompress(&nifs_secondary.comm_T)?), ); @@ -474,8 +492,13 @@ where &pp.circuit_shape_primary.r1cs_shape, &self.r_U_primary, &self.r_W_primary, - &l_u_primary, - &l_w_primary, + &RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_primary, + &pp.circuit_shape_primary.r1cs_shape, + &l_u_primary, + ), + &RelaxedR1CSWitness::from_r1cs_witness(&pp.circuit_shape_primary.r1cs_shape, &l_w_primary), + false, ) .expect("Unable to fold primary"); @@ -486,7 +509,11 @@ where z0_secondary, Some(self.zi_secondary.clone()), Some(self.r_U_primary.clone()), - Some(l_u_primary), + Some(RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_primary, + &pp.circuit_shape_primary.r1cs_shape, + &l_u_primary, + )), Some(Commitment::::decompress(&nifs_primary.comm_T)?), ); @@ -703,7 +730,7 @@ where r_W_snark_primary: S1, r_U_secondary: RelaxedR1CSInstance, - l_u_secondary: R1CSInstance, + l_u_secondary: RelaxedR1CSInstance, nifs_secondary: NIFS, f_W_snark_secondary: S2, @@ -773,8 +800,16 @@ where &pp.circuit_shape_secondary.r1cs_shape, &recursive_snark.r_U_secondary, &recursive_snark.r_W_secondary, - &recursive_snark.l_u_secondary, - &recursive_snark.l_w_secondary, + &RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_secondary, + &pp.circuit_shape_secondary.r1cs_shape, + &recursive_snark.l_u_secondary, + ), + &RelaxedR1CSWitness::from_r1cs_witness( + &pp.circuit_shape_secondary.r1cs_shape, + &recursive_snark.l_w_secondary, + ), + false, ); let (nifs_secondary, (f_U_secondary, f_W_secondary)) = res_secondary?; @@ -806,7 +841,11 @@ where r_W_snark_primary: r_W_snark_primary?, r_U_secondary: recursive_snark.r_U_secondary.clone(), - l_u_secondary: recursive_snark.l_u_secondary.clone(), + l_u_secondary: RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_secondary, + &pp.circuit_shape_secondary.r1cs_shape, + &recursive_snark.l_u_secondary, + ), nifs_secondary, f_W_snark_secondary: f_W_snark_secondary?, @@ -887,6 +926,7 @@ where &scalar_as_base::(vk.pp_digest), &self.r_U_secondary, &self.l_u_secondary, + false, )?; // check the satisfiability of the folded instances using SNARKs proving the knowledge of their satisfying witnesses @@ -954,7 +994,7 @@ mod tests { use ::bellpepper_core::{num::AllocatedNum, ConstraintSystem, SynthesisError}; use core::marker::PhantomData; use ff::PrimeField; - use traits::circuit::TrivialCircuit; + use traits::circuit::{OutputStepCircuit, TrivialCircuit}; #[derive(Clone, Debug, Default)] struct CubicCircuit { @@ -1001,6 +1041,18 @@ mod tests { } } + impl OutputStepCircuit for CubicCircuit + where + F: PrimeField, + { + fn output(&self, z: &[F]) -> Vec { + let x = &z[0]; + let x_sq = x.square(); + let x_cu = *x * x_sq; + vec![x_cu] + } + } + impl CubicCircuit where F: PrimeField, @@ -1655,4 +1707,39 @@ mod tests { test_ivc_base_with::(); test_ivc_base_with::(); } + + #[test] + fn test_parallel_ivc_nontrivial() { + type G1 = pasta_curves::pallas::Point; + type G2 = pasta_curves::vesta::Point; + + let circuit_primary = TrivialCircuit::default(); + let circuit_secondary = CubicCircuit::default(); + + // produce public parameters + let pp = parallel_prover::PublicParams::< + G1, + G2, + TrivialCircuit<::Scalar>, + CubicCircuit<::Scalar>, + >::setup( + circuit_primary.clone(), + circuit_secondary.clone(), + None, + None, + ); + + let num_steps = 20; + + let mut prover = parallel_prover::ParallelSNARK::new( + &pp, + num_steps, + vec![::Scalar::one()], + vec![::Scalar::zero()], + circuit_primary.clone(), + circuit_secondary.clone(), + ); + + prover.prove(&pp, &circuit_primary, &circuit_secondary); + } } diff --git a/src/nifs.rs b/src/nifs.rs index d2b8872a0..714791686 100644 --- a/src/nifs.rs +++ b/src/nifs.rs @@ -1,14 +1,16 @@ //! This module implements a non-interactive folding scheme #![allow(non_snake_case)] +#![allow(clippy::type_complexity)] use crate::{ constants::{NUM_CHALLENGE_BITS, NUM_FE_FOR_RO}, errors::NovaError, - r1cs::{R1CSInstance, R1CSShape, R1CSWitness, RelaxedR1CSInstance, RelaxedR1CSWitness}, + r1cs::{R1CSShape, RelaxedR1CSInstance, RelaxedR1CSWitness}, scalar_as_base, traits::{commitment::CommitmentTrait, AbsorbInROTrait, Group, ROTrait}, Commitment, CommitmentKey, CompressedCommitment, }; +use core::marker::PhantomData; use serde::{Deserialize, Serialize}; /// A SNARK that holds the proof of a step of an incremental computation @@ -17,6 +19,7 @@ use serde::{Deserialize, Serialize}; #[serde(bound = "")] pub struct NIFS { pub(crate) comm_T: CompressedCommitment, + _p: PhantomData, } type ROConstants = @@ -29,8 +32,6 @@ impl NIFS { /// a folded Relaxed R1CS instance-witness tuple `(U, W)` of the same shape `shape`, /// with the guarantee that the folded witness `W` satisfies the folded instance `U` /// if and only if `W1` satisfies `U1` and `W2` satisfies `U2`. - #[allow(clippy::too_many_arguments)] - #[tracing::instrument(skip_all, level = "trace", name = "NIFS::prove")] pub fn prove( ck: &CommitmentKey, ro_consts: &ROConstants, @@ -38,18 +39,28 @@ impl NIFS { S: &R1CSShape, U1: &RelaxedR1CSInstance, W1: &RelaxedR1CSWitness, - U2: &R1CSInstance, - W2: &R1CSWitness, + U2: &RelaxedR1CSInstance, + W2: &RelaxedR1CSWitness, + as_relaxed: bool, ) -> Result<(NIFS, (RelaxedR1CSInstance, RelaxedR1CSWitness)), NovaError> { // initialize a new RO - let mut ro = G::RO::new(ro_consts.clone(), NUM_FE_FOR_RO); + let num_absorbs = if as_relaxed { + NUM_FE_FOR_RO + 10 + } else { + NUM_FE_FOR_RO + }; + let mut ro = G::RO::new(ro_consts.clone(), num_absorbs); // append the digest of pp to the transcript ro.absorb(scalar_as_base::(*pp_digest)); // append U1 and U2 to transcript U1.absorb_in_ro(&mut ro); - U2.absorb_in_ro(&mut ro); + if as_relaxed { + U2.absorb_in_ro(&mut ro); + } else { + U2.absorb_in_ro_as_strict(&mut ro); + } // compute a commitment to the cross-term let (T, comm_T) = S.commit_T(ck, U1, W1, U2, W2)?; @@ -70,12 +81,13 @@ impl NIFS { Ok(( Self { comm_T: comm_T.compress(), + _p: Default::default(), }, (U, W), )) } - /// Takes as input a relaxed R1CS instance `U1` and R1CS instance `U2` + /// Takes as input a relaxed R1CS instance `U1` and and R1CS instance `U2` /// with the same shape and defined with respect to the same parameters, /// and outputs a folded instance `U` with the same shape, /// with the guarantee that the folded instance `U` @@ -85,17 +97,27 @@ impl NIFS { ro_consts: &ROConstants, pp_digest: &G::Scalar, U1: &RelaxedR1CSInstance, - U2: &R1CSInstance, + U2: &RelaxedR1CSInstance, + as_relaxed: bool, ) -> Result, NovaError> { // initialize a new RO - let mut ro = G::RO::new(ro_consts.clone(), NUM_FE_FOR_RO); + let num_absorbs = if as_relaxed { + NUM_FE_FOR_RO + 10 + } else { + NUM_FE_FOR_RO + }; + let mut ro = G::RO::new(ro_consts.clone(), num_absorbs); // append the digest of pp to the transcript ro.absorb(scalar_as_base::(*pp_digest)); // append U1 and U2 to transcript U1.absorb_in_ro(&mut ro); - U2.absorb_in_ro(&mut ro); + if as_relaxed { + U2.absorb_in_ro(&mut ro); + } else { + U2.absorb_in_ro_as_strict(&mut ro); + } // append `comm_T` to the transcript and obtain a challenge let comm_T = Commitment::::decompress(&self.comm_T)?; @@ -118,6 +140,7 @@ mod tests { use crate::{ r1cs::{commitment_key, SparseMatrix}, traits::Group, + R1CSInstance, R1CSWitness, RelaxedR1CSInstance, RelaxedR1CSWitness, }; use ::bellpepper_core::{num::AllocatedNum, ConstraintSystem, SynthesisError}; use ff::{Field, PrimeField}; @@ -180,16 +203,22 @@ mod tests { let _ = synthesize_tiny_r1cs_bellpepper(&mut cs, Some(G::Scalar::from(5))); let (U1, W1) = cs.r1cs_instance_and_witness(&shape, &ck).unwrap(); + let U1 = RelaxedR1CSInstance::from_r1cs_instance(&ck, &shape, &U1); + let W1 = RelaxedR1CSWitness::from_r1cs_witness(&shape, &W1); + // Make sure that the first instance is satisfiable - assert!(shape.is_sat(&ck, &U1, &W1).is_ok()); + assert!(shape.is_sat_relaxed(&ck, &U1, &W1).is_ok()); // Now get the instance and assignment for second instance let mut cs: SatisfyingAssignment = SatisfyingAssignment::new(); let _ = synthesize_tiny_r1cs_bellpepper(&mut cs, Some(G::Scalar::from(135))); let (U2, W2) = cs.r1cs_instance_and_witness(&shape, &ck).unwrap(); + let U2 = RelaxedR1CSInstance::from_r1cs_instance(&ck, &shape, &U2); + let W2 = RelaxedR1CSWitness::from_r1cs_witness(&shape, &W2); + // Make sure that the second instance is satisfiable - assert!(shape.is_sat(&ck, &U2, &W2).is_ok()); + assert!(shape.is_sat_relaxed(&ck, &U2, &W2).is_ok()); // execute a sequence of folds execute_sequence( @@ -216,10 +245,10 @@ mod tests { ro_consts: &<::RO as ROTrait<::Base, ::Scalar>>::Constants, pp_digest: &::Scalar, shape: &R1CSShape, - U1: &R1CSInstance, - W1: &R1CSWitness, - U2: &R1CSInstance, - W2: &R1CSWitness, + U1: &RelaxedR1CSInstance, + W1: &RelaxedR1CSWitness, + U2: &RelaxedR1CSInstance, + W2: &RelaxedR1CSWitness, ) where G: Group, { @@ -228,12 +257,12 @@ mod tests { let mut r_U = RelaxedR1CSInstance::default(ck, shape); // produce a step SNARK with (W1, U1) as the first incoming witness-instance pair - let res = NIFS::prove(ck, ro_consts, pp_digest, shape, &r_U, &r_W, U1, W1); + let res = NIFS::prove(ck, ro_consts, pp_digest, shape, &r_U, &r_W, U1, W1, false); assert!(res.is_ok()); let (nifs, (_U, W)) = res.unwrap(); // verify the step SNARK with U1 as the first incoming instance - let res = nifs.verify(ro_consts, pp_digest, &r_U, U1); + let res = nifs.verify(ro_consts, pp_digest, &r_U, U1, false); assert!(res.is_ok()); let U = res.unwrap(); @@ -244,12 +273,12 @@ mod tests { r_U = U; // produce a step SNARK with (W2, U2) as the second incoming witness-instance pair - let res = NIFS::prove(ck, ro_consts, pp_digest, shape, &r_U, &r_W, U2, W2); + let res = NIFS::prove(ck, ro_consts, pp_digest, shape, &r_U, &r_W, U2, W2, false); assert!(res.is_ok()); let (nifs, (_U, W)) = res.unwrap(); // verify the step SNARK with U1 as the first incoming instance - let res = nifs.verify(ro_consts, pp_digest, &r_U, U2); + let res = nifs.verify(ro_consts, pp_digest, &r_U, U2, false); assert!(res.is_ok()); let U = res.unwrap(); @@ -337,7 +366,9 @@ mod tests { <::RO as ROTrait<::Base, ::Scalar>>::Constants::default(); let rand_inst_witness_generator = - |ck: &CommitmentKey, I: &G::Scalar| -> (G::Scalar, R1CSInstance, R1CSWitness) { + |ck: &CommitmentKey, + I: &G::Scalar| + -> (G::Scalar, RelaxedR1CSInstance, RelaxedR1CSWitness) { let i0 = *I; // compute a satisfying (vars, X) tuple @@ -365,8 +396,11 @@ mod tests { res.unwrap() }; + let U = RelaxedR1CSInstance::from_r1cs_instance(ck, &S, &U); + let W = RelaxedR1CSWitness::from_r1cs_witness(&S, &W); + // check that generated instance is satisfiable - assert!(S.is_sat(ck, &U, &W).is_ok()); + assert!(S.is_sat_relaxed(ck, &U, &W).is_ok()); (O, U, W) }; diff --git a/src/parallel_circuit.rs b/src/parallel_circuit.rs new file mode 100644 index 000000000..79131fcd6 --- /dev/null +++ b/src/parallel_circuit.rs @@ -0,0 +1,586 @@ +//! There are two Verification Circuits. The primary and the secondary. +//! Each of them is over a Pasta curve but +//! only the primary executes the next step of the computation. +//! We have two running instances. Each circuit takes as input 2 hashes: one for each +//! of the running instances. Each of these hashes is +//! H(params = H(shape, ck), i, z0, zi, U). Each circuit folds the last invocation of +//! the other into the running instance + +#![allow(unused)] +use crate::{ + circuit::NovaAugmentedCircuitParams, + constants::{NUM_FE_WITHOUT_IO_FOR_CRHF, NUM_HASH_BITS}, + gadgets::{ + ecc::AllocatedPoint, + r1cs::{AllocatedR1CSInstance, AllocatedRelaxedR1CSInstance}, + utils::{alloc_num_equals, alloc_scalar_as_base, conditionally_select_vec, le_bits_to_num}, + }, + r1cs::RelaxedR1CSInstance, + traits::{ + circuit::StepCircuit, commitment::CommitmentTrait, Group, ROCircuitTrait, ROConstantsCircuit, + }, + Commitment, +}; +use bellpepper::gadgets::{ + boolean::{AllocatedBit, Boolean}, + Assignment, +}; +use bellpepper_core::{num::AllocatedNum, Circuit, ConstraintSystem, SynthesisError}; +use ff::Field; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct NovaAugmentedParallelCircuitInputs { + params: G::Scalar, // Hash(Shape of u2, Gens for u2). Needed for computing the challenge. + i_start_U: G::Base, + i_end_U: G::Base, + i_start_R: G::Base, + i_end_R: G::Base, + z_U_start: Vec, + z_U_end: Vec, + z_R_start: Vec, + z_R_end: Vec, + U: Option>, + u: Option>, + R: Option>, + r: Option>, + T_u: Option>, + T_r: Option>, + T_R_U: Option>, +} + +impl NovaAugmentedParallelCircuitInputs { + /// Create new inputs/witness for the verification circuit + #[allow(clippy::too_many_arguments)] + // Remove when we write the struct implementing the parallel nova instance + #[allow(unused)] + pub fn new( + params: G::Scalar, // Hash(Shape of u2, Gens for u2). Needed for computing the challenge. + i_start_U: G::Base, + i_end_U: G::Base, + i_start_R: G::Base, + i_end_R: G::Base, + z_U_start: Vec, + z_U_end: Vec, + z_R_start: Vec, + z_R_end: Vec, + U: Option>, + u: Option>, + R: Option>, + r: Option>, + T_u: Option>, + T_r: Option>, + T_R_U: Option>, + ) -> Self { + Self { + params, + i_start_U, + i_end_U, + i_start_R, + i_end_R, + z_U_start, + z_U_end, + z_R_start, + z_R_end, + U, + u, + R, + r, + T_u, + T_r, + T_R_U, + } + } +} + +/// 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 NovaAugmentedParallelCircuit> { + params: NovaAugmentedCircuitParams, + ro_consts: ROConstantsCircuit, + inputs: Option>, + step_circuit: SC, // The function that is applied for each step +} + +impl> NovaAugmentedParallelCircuit { + /// Create a new verification circuit for the input relaxed r1cs instances + // Remove when we write the struct implementing the parallel nova instance + #[allow(unused)] + pub fn new( + params: NovaAugmentedCircuitParams, + inputs: Option>, + step_circuit: SC, + ro_consts: ROConstantsCircuit, + ) -> Self { + Self { + params, + inputs, + step_circuit, + ro_consts, + } + } + + /// Allocate all witnesses and return + fn alloc_witness::Base>>( + &self, + mut cs: CS, + arity: usize, + ) -> Result< + ( + AllocatedNum, + AllocatedNum, + AllocatedNum, + AllocatedNum, + AllocatedNum, + Vec>, + Vec>, + Vec>, + Vec>, + AllocatedRelaxedR1CSInstance, + AllocatedR1CSInstance, + AllocatedRelaxedR1CSInstance, + AllocatedR1CSInstance, + AllocatedPoint, + AllocatedPoint, + AllocatedPoint, + usize, + ), + SynthesisError, + > { + // Allocate the params + let params = alloc_scalar_as_base::( + cs.namespace(|| "params"), + self.inputs.get().map_or(None, |inputs| Some(inputs.params)), + )?; + + // Allocate idexes + let i_start_U = AllocatedNum::alloc(cs.namespace(|| "i_start_U"), || { + Ok(self.inputs.get()?.i_start_U) + })?; + let i_end_U = AllocatedNum::alloc(cs.namespace(|| "i_end_U"), || { + Ok(self.inputs.get()?.i_end_U) + })?; + let i_start_R = AllocatedNum::alloc(cs.namespace(|| "i_start_R"), || { + Ok(self.inputs.get()?.i_start_R) + })?; + let i_end_R = AllocatedNum::alloc(cs.namespace(|| "i_end_R"), || { + Ok(self.inputs.get()?.i_end_R) + })?; + + // Allocate input and output vectors + let z_U_start = (0..arity) + .map(|i| { + AllocatedNum::alloc(cs.namespace(|| format!("z_U_start_{i}")), || { + Ok(self.inputs.get()?.z_U_start[i]) + }) + }) + .collect::>, _>>()?; + let z_U_end = (0..arity) + .map(|i| { + AllocatedNum::alloc(cs.namespace(|| format!("z_U_end_{i}")), || { + Ok(self.inputs.get()?.z_U_end[i]) + }) + }) + .collect::>, _>>()?; + // Allocate z_R_start + let z_R_start = (0..arity) + .map(|i| { + AllocatedNum::alloc(cs.namespace(|| format!("z_R_start_{i}")), || { + Ok(self.inputs.get()?.z_R_start[i]) + }) + }) + .collect::>, _>>()?; + // Allocate z_R_end + let z_R_end = (0..arity) + .map(|i| { + AllocatedNum::alloc(cs.namespace(|| format!("z_R_end_{i}")), || { + Ok(self.inputs.get()?.z_R_end[i]) + }) + }) + .collect::>, _>>()?; + + // Allocate the running instance U + let U: AllocatedRelaxedR1CSInstance = AllocatedRelaxedR1CSInstance::alloc( + cs.namespace(|| "Allocate U"), + self.inputs.get().map_or(None, |inputs| inputs.U.get().ok()), + self.params.limb_width, + self.params.n_limbs, + )?; + + // Allocate the instance u to be folded in + let u = AllocatedR1CSInstance::alloc( + cs.namespace(|| "allocate instance u to fold"), + self.inputs.get().map_or(None, |inputs| inputs.u.get().ok()), + )?; + + // Allocate the running instance U + let R: AllocatedRelaxedR1CSInstance = AllocatedRelaxedR1CSInstance::alloc( + cs.namespace(|| "Allocate R"), + self.inputs.get().map_or(None, |inputs| inputs.R.get().ok()), + self.params.limb_width, + self.params.n_limbs, + )?; + + // Allocate the instance r to be folded in + let r = AllocatedR1CSInstance::alloc( + cs.namespace(|| "allocate instance r to fold"), + self.inputs.get().map_or(None, |inputs| inputs.r.get().ok()), + )?; + + // Allocate T + let T_u = AllocatedPoint::alloc( + cs.namespace(|| "allocate T_u"), + self.inputs.get().map_or(None, |inputs| { + inputs + .T_r + .get() + .map_or(None, |T_r| Some(T_r.to_coordinates())) + }), + )?; + + let T_r = AllocatedPoint::alloc( + cs.namespace(|| "allocate T_r"), + self.inputs.get().map_or(None, |inputs| { + inputs + .T_u + .get() + .map_or(None, |T_u| Some(T_u.to_coordinates())) + }), + )?; + + let T_R_U = AllocatedPoint::alloc( + cs.namespace(|| "allocate T_R_U"), + self.inputs.get().map_or(None, |inputs| { + inputs + .T_R_U + .get() + .map_or(None, |T_R_U| Some(T_R_U.to_coordinates())) + }), + )?; + + Ok(( + params, i_start_U, i_end_U, i_start_R, i_end_R, z_U_start, z_U_end, z_R_start, z_R_end, U, u, + R, r, T_u, T_r, T_R_U, arity, + )) + } + + /// Synthesizes base case and returns the new relaxed R1CSInstance + fn synthesize_base_case::Base>>( + &self, + mut cs: CS, + u: AllocatedR1CSInstance, + ) -> Result, SynthesisError> { + let U_default: AllocatedRelaxedR1CSInstance = if self.params.is_primary_circuit { + // The primary circuit just returns the default R1CS instance + AllocatedRelaxedR1CSInstance::default( + cs.namespace(|| "Allocate U_default"), + self.params.limb_width, + self.params.n_limbs, + )? + } else { + // The secondary circuit returns the incoming R1CS instance + AllocatedRelaxedR1CSInstance::from_r1cs_instance( + cs.namespace(|| "Allocate U_default"), + u, + self.params.limb_width, + self.params.n_limbs, + )? + }; + Ok(U_default) + } + + /// Synthesizes non base case and returns the new relaxed R1CSInstance + /// And a boolean indicating if all checks pass + #[allow(clippy::too_many_arguments)] + fn synthesize_non_base_case::Base>>( + &self, + mut cs: CS, + params: AllocatedNum, + i_start_U: AllocatedNum, + i_end_U: AllocatedNum, + i_start_R: AllocatedNum, + i_end_R: AllocatedNum, + z_U_start: Vec>, + z_U_end: Vec>, + z_R_start: Vec>, + z_R_end: Vec>, + U: AllocatedRelaxedR1CSInstance, + u: AllocatedR1CSInstance, + R: AllocatedRelaxedR1CSInstance, + r: AllocatedR1CSInstance, + T_u: AllocatedPoint, + T_r: AllocatedPoint, + T_R_U: AllocatedPoint, + arity: usize, + ) -> Result<(AllocatedRelaxedR1CSInstance, AllocatedBit), SynthesisError> { + // Check that u.x[0] = Hash(params, i_start_U, i_end_U z_U_start, z_U_end, U) + let mut ro = G::ROCircuit::new( + self.ro_consts.clone(), + NUM_FE_WITHOUT_IO_FOR_CRHF + 2 * arity + 1, + ); + ro.absorb(¶ms); + ro.absorb(&i_start_U); + ro.absorb(&i_end_U); + + for e in z_U_start.clone() { + ro.absorb(&e); + } + for e in z_U_end { + ro.absorb(&e); + } + U.absorb_in_ro(cs.namespace(|| "absorb U"), &mut ro)?; + + let hash_bits = ro.squeeze(cs.namespace(|| "Input hash first"), NUM_HASH_BITS)?; + let hash_u = le_bits_to_num(cs.namespace(|| "bits to hash first"), &hash_bits)?; + + let check_pass_u = alloc_num_equals( + cs.namespace(|| "check consistency of u.X[0] with H(params, U, i, z_u_start, z_u_end)"), + &u.X0, + &hash_u, + )?; + + // Run NIFS Verifier + let U_fold = U.fold_with_r1cs( + cs.namespace(|| "compute fold of U and u"), + ¶ms, + &u, + &T_u, + self.ro_consts.clone(), + self.params.limb_width, + self.params.n_limbs, + )?; + + // Check that r.x[0] = Hash(params, i_start_R, i_end_R, z_R_start, z_R_end, R) + ro = G::ROCircuit::new( + self.ro_consts.clone(), + NUM_FE_WITHOUT_IO_FOR_CRHF + 2 * arity + 1, + ); + ro.absorb(¶ms); + ro.absorb(&i_start_R); + ro.absorb(&i_end_R); + for e in z_R_start.clone() { + ro.absorb(&e); + } + for e in z_R_end.clone() { + ro.absorb(&e); + } + R.absorb_in_ro(cs.namespace(|| "absorb R"), &mut ro)?; + + let hash_bits = ro.squeeze(cs.namespace(|| "Input hash second"), NUM_HASH_BITS)?; + let hash_r = le_bits_to_num(cs.namespace(|| "bits to hash second"), &hash_bits)?; + let check_pass_r = alloc_num_equals( + cs.namespace(|| "check consistency of r.X[0] with H(params, R, i, z_r_start, z_r_end)"), + &r.X0, + &hash_r, + )?; + + // Run NIFS Verifier + let R_fold = R.fold_with_r1cs( + cs.namespace(|| "compute fold of R and r"), + ¶ms, + &r, + &T_r, + self.ro_consts.clone(), + self.params.limb_width, + self.params.n_limbs, + )?; + + // Run NIFS Verifier + let U_R_fold = U_fold.fold_with_relaxed_r1cs( + cs.namespace(|| "compute fold of U and R"), + params, + R_fold, + T_R_U, + self.ro_consts.clone(), + self.params.limb_width, + self.params.n_limbs, + )?; + + let hashChecks = AllocatedBit::and( + cs.namespace(|| "check both hashes are correct"), + &check_pass_u, + &check_pass_r, + )?; + + Ok((U_R_fold, hashChecks)) + } +} + +impl> Circuit<::Base> + for NovaAugmentedParallelCircuit +{ + fn synthesize::Base>>( + self, + cs: &mut CS, + ) -> Result<(), SynthesisError> { + let arity = self.step_circuit.arity(); + + // Allocate all witnesses + let ( + params, + i_start_U, + i_end_U, + i_start_R, + i_end_R, + z_U_start, + z_U_end, + z_R_start, + z_R_end, + U, + u, + R, + r, + T_u, + T_r, + T_R_U, + arity, + ) = self.alloc_witness(cs.namespace(|| "allocate the circuit witness"), arity)?; + + // Compute variable indicating if this is the base case + let mut is_base_case = alloc_num_equals( + cs.namespace(|| "Check if base case as i_start_U == i_end_U"), + &i_start_U.clone(), + &i_end_U, + )?; + let r_index_equal = alloc_num_equals( + cs.namespace(|| "In base case i_start_R == i_end_R"), + &i_start_R, + &i_end_R, + )?; + is_base_case = AllocatedBit::and( + cs.namespace(|| "i_start_U == i_end_U and i_start_U + 1 == i_end_R"), + &is_base_case, + &r_index_equal, + )?; + + // Synthesize the circuit for the base case and get the new running instance + let Unew_base = self.synthesize_base_case(cs.namespace(|| "base case"), u.clone())?; + + // Synthesize the circuit for the non-base case and get the new running + // instance along with a boolean indicating if all checks have passed + let (Unew_non_base, check_non_base_pass) = self.synthesize_non_base_case( + cs.namespace(|| "synthesize non base case"), + params.clone(), + i_start_U.clone(), + i_end_U.clone(), + i_start_R.clone(), + i_end_R.clone(), + z_U_start.clone(), + z_U_end.clone(), + z_R_start.clone(), + z_R_end.clone(), + U, + u.clone(), + R, + r.clone(), + T_u, + T_r, + T_R_U, + arity, + )?; + + // Either check_non_base_pass=true or we are in the base case + let should_be_false = AllocatedBit::nor( + cs.namespace(|| "check_non_base_pass nor base_case"), + &check_non_base_pass, + &is_base_case, + )?; + cs.enforce( + || "check_non_base_pass nor base_case = false", + |lc| lc + should_be_false.get_variable(), + |lc| lc + CS::one(), + |lc| lc, + ); + + // Compute the U_new + let Unew = Unew_base.conditionally_select( + cs.namespace(|| "compute U_new"), + &Unew_non_base, + &Boolean::from(is_base_case.clone()), + )?; + + // Compute i_u_end + 1 == i_r_start, this enforces only one invocation of F between ranges + let i_new = AllocatedNum::alloc(cs.namespace(|| "i + 1"), || { + Ok(*i_end_U.get_value().get()? + G::Base::ONE) + })?; + cs.enforce( + || "check i + 1", + |lc| lc, + |lc| lc, + |lc| lc + i_new.get_variable() - i_start_R.get_variable(), + ); + + // The input to the F function is either z_U_end in default case or z_U_start in the base case + let z_input = conditionally_select_vec( + cs.namespace(|| "select input to F"), + &z_U_start, + &z_U_end, + &Boolean::from(is_base_case.clone()), + )?; + + let z_next = self + .step_circuit + .synthesize(&mut cs.namespace(|| "F"), &z_input)?; + + // In the base case our output is in the z_R_end field and in our normal case it's in z_R_start + let z_output = conditionally_select_vec( + cs.namespace(|| "select output of F"), + &z_R_end, + &z_R_start, + &Boolean::from(is_base_case), + )?; + + if z_next.len() != arity { + return Err(SynthesisError::IncompatibleLengthVector( + "z_next".to_string(), + )); + } + + // Check vector equality of the step output and of the start of the R block + let mut outputEqual = AllocatedBit::alloc(cs.namespace(|| "allocate bit equal"), Some(true))?; + for (next, input) in z_next.clone().iter().zip(z_output.clone().iter()) { + // We check that each index is equal then and it with the global check + let entryEqual = alloc_num_equals( + cs.namespace(|| "equality check of z_next and z_output"), + next, + input, + )?; + outputEqual = AllocatedBit::and( + cs.namespace(|| "accumulate equality checks"), + &outputEqual, + &entryEqual, + )?; + } + + Boolean::enforce_equal( + &mut cs.namespace(|| "outputEqual is true"), + &Boolean::Is(outputEqual), + &Boolean::Constant(true), + )?; + + // Compute the new hash H(params, Unew, i_u_start, z_U_start, z_R_end) + let mut ro = G::ROCircuit::new(self.ro_consts, NUM_FE_WITHOUT_IO_FOR_CRHF + 2 * arity + 1); + ro.absorb(¶ms); + ro.absorb(&i_start_U.clone()); + ro.absorb(&i_end_R.clone()); + for e in z_input { + ro.absorb(&e); + } + for e in z_output { + ro.absorb(&e); + } + Unew.absorb_in_ro(cs.namespace(|| "absorb U_new"), &mut ro)?; + + let hash_bits = ro.squeeze(cs.namespace(|| "output hash bits"), NUM_HASH_BITS)?; + let hash = le_bits_to_num(cs.namespace(|| "convert hash to num"), &hash_bits)?; + + // Outputs the computed hash and u.X[1] that corresponds to the hash of the other circuit + hash.inputize(cs.namespace(|| "output new hash of this circuit"))?; + u.X1 + .inputize(cs.namespace(|| "Output unmodified hash of the u circuit"))?; + // r.X1.inputize(cs.namespace(|| "Output unmodified hash of the r circuit"))?; + + Ok(()) + } +} diff --git a/src/parallel_prover.rs b/src/parallel_prover.rs new file mode 100644 index 000000000..6cfea90f5 --- /dev/null +++ b/src/parallel_prover.rs @@ -0,0 +1,847 @@ +#![allow(unused_imports)] +#![allow(unused)] +//! There are two Verification Circuits. The primary and the secondary. +//! Each of them is over a Pasta curve but +//! only the primary executes the next step of the computation. +//! Each recursive tree node has both an aggregated and new instance +//! of both the primary and secondary circuit. As you merge the nodes +//! the proofs verify that three folding are correct and merge the +//! running instances and new instances in a pair of nodes to a single +//! running instance. +//! We check that hash(index start, index end, z_start, z_end) has been +//! committed properly for each node. +//! The circuit also checks that when F is executed on the left nodes +//! z_end that the output is z_start of the right node + +use abomonation::Abomonation; +use abomonation_derive::Abomonation; + +use crate::{ + bellpepper::{ + r1cs::{NovaShape, NovaWitness}, + shape_cs::ShapeCS, + solver::SatisfyingAssignment, + }, + circuit::NovaAugmentedCircuitParams, + constants::{BN_LIMB_WIDTH, BN_N_LIMBS}, + constants::{NUM_FE_WITHOUT_IO_FOR_CRHF, NUM_HASH_BITS}, + digest::{DigestComputer, SimpleDigestible}, + errors::NovaError, + gadgets::{ + ecc::AllocatedPoint, + r1cs::{AllocatedR1CSInstance, AllocatedRelaxedR1CSInstance}, + utils::{alloc_num_equals, alloc_scalar_as_base, conditionally_select_vec, le_bits_to_num}, + }, + nifs::NIFS, + parallel_circuit::{NovaAugmentedParallelCircuit, NovaAugmentedParallelCircuitInputs}, + r1cs::{CommitmentKeyHint, R1CSShape, RelaxedR1CSInstance, RelaxedR1CSWitness}, + scalar_as_base, + traits::{ + circuit::{OutputStepCircuit, StepCircuit}, + commitment::{CommitmentEngineTrait, CommitmentTrait}, + snark::RelaxedR1CSSNARKTrait, + AbsorbInROTrait, Group, ROConstants, ROConstantsCircuit, ROTrait, + }, + Commitment, +}; +use bellpepper::gadgets::{ + boolean::{AllocatedBit, Boolean}, + Assignment, +}; +use bellpepper_core::{num::AllocatedNum, Circuit, ConstraintSystem, Index, SynthesisError}; +use core::marker::PhantomData; +use ff::{Field, PrimeField}; +use once_cell::sync::OnceCell; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; + +// TODO - This is replicated from lib but we should actually instead have another file for it and use both here and there + +type CommitmentKey = <::CE as CommitmentEngineTrait>::CommitmentKey; + +/// A type that holds public parameters of Nova +#[derive(Clone, PartialEq, Serialize, Deserialize, Abomonation)] +#[serde(bound = "")] +#[abomonation_bounds( +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit, + C2: StepCircuit, + ::Repr: Abomonation, + ::Repr: Abomonation, +)] + +pub struct PublicParams +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit, + C2: StepCircuit, +{ + F_arity_primary: usize, + F_arity_secondary: usize, + ro_consts_primary: ROConstants, + ro_consts_circuit_primary: ROConstantsCircuit, + ck_primary: CommitmentKey, + r1cs_shape_primary: R1CSShape, + ro_consts_secondary: ROConstants, + ro_consts_circuit_secondary: ROConstantsCircuit, + ck_secondary: CommitmentKey, + r1cs_shape_secondary: R1CSShape, + augmented_circuit_params_primary: NovaAugmentedCircuitParams, + augmented_circuit_params_secondary: NovaAugmentedCircuitParams, + #[abomonation_skip] + #[serde(skip, default = "OnceCell::new")] + digest: OnceCell, + _p_c1: PhantomData, + _p_c2: PhantomData, +} + +impl SimpleDigestible for PublicParams +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit, + C2: StepCircuit, +{ +} + +impl PublicParams +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit, + C2: StepCircuit, +{ + /// Create a new `PublicParams` + pub fn setup( + c_primary: C1, + c_secondary: C2, + optfn1: Option>, + optfn2: Option>, + ) -> Self { + let augmented_circuit_params_primary = + NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, true); + let augmented_circuit_params_secondary = + NovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, false); + + let ro_consts_primary: ROConstants = ROConstants::::default(); + let ro_consts_secondary: ROConstants = ROConstants::::default(); + + let F_arity_primary = c_primary.arity(); + let F_arity_secondary = c_secondary.arity(); + + // ro_consts_circuit_primary are parameterized by G2 because the type alias uses G2::Base = G1::Scalar + let ro_consts_circuit_primary: ROConstantsCircuit = ROConstantsCircuit::::default(); + let ro_consts_circuit_secondary: ROConstantsCircuit = ROConstantsCircuit::::default(); + + // Initialize ck for the primary + let circuit_primary: NovaAugmentedParallelCircuit = NovaAugmentedParallelCircuit::new( + augmented_circuit_params_primary.clone(), + None, + c_primary, + ro_consts_circuit_primary.clone(), + ); + let mut cs: ShapeCS = ShapeCS::new(); + let _ = circuit_primary.synthesize(&mut cs); + let (r1cs_shape_primary, ck_primary) = cs.r1cs_shape_and_key(optfn1); + + // Initialize ck for the secondary + let circuit_secondary: NovaAugmentedParallelCircuit = NovaAugmentedParallelCircuit::new( + augmented_circuit_params_secondary.clone(), + None, + c_secondary, + ro_consts_circuit_secondary.clone(), + ); + let mut cs: ShapeCS = ShapeCS::new(); + let _ = circuit_secondary.synthesize(&mut cs); + let (r1cs_shape_secondary, ck_secondary) = cs.r1cs_shape_and_key(optfn2); + + Self { + F_arity_primary, + F_arity_secondary, + ro_consts_primary, + ro_consts_circuit_primary, + ck_primary, + r1cs_shape_primary, + ro_consts_secondary, + ro_consts_circuit_secondary, + ck_secondary, + r1cs_shape_secondary, + augmented_circuit_params_primary, + augmented_circuit_params_secondary, + digest: OnceCell::new(), + _p_c1: Default::default(), + _p_c2: Default::default(), + } + } + + /// Retrieve the digest of the public parameters. + pub fn digest(&self) -> G1::Scalar { + self + .digest + .get_or_try_init(|| DigestComputer::new(self).digest()) + .cloned() + .expect("Failure in retrieving digest") + } + + /// Returns the number of constraints in the primary and secondary circuits + pub fn num_constraints(&self) -> (usize, usize) { + ( + self.r1cs_shape_primary.num_cons, + self.r1cs_shape_secondary.num_cons, + ) + } + + /// Returns the number of variables in the primary and secondary circuits + pub fn num_variables(&self) -> (usize, usize) { + ( + self.r1cs_shape_primary.num_vars, + self.r1cs_shape_secondary.num_vars, + ) + } +} + +// This ends the 1 to 1 copied code + +/// A type that holds one node the tree based nova proof. This will have both running instances and fresh instances +/// of the primary and secondary circuit. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct NovaTreeNode +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit, + C2: StepCircuit, +{ + // The running instance of the primary + W_primary: RelaxedR1CSWitness, + U_primary: RelaxedR1CSInstance, + // The new instance of the primary + w_primary: RelaxedR1CSWitness, + u_primary: RelaxedR1CSInstance, + // The running instance of the secondary + W_secondary: RelaxedR1CSWitness, + U_secondary: RelaxedR1CSInstance, + // The running instance of the secondary + w_secondary: RelaxedR1CSWitness, + u_secondary: RelaxedR1CSInstance, + i_start: u64, + i_end: u64, + z_start_primary: Vec, + z_end_primary: Vec, + z_start_secondary: Vec, + z_end_secondary: Vec, + _p_c1: PhantomData, + _p_c2: PhantomData, +} + +impl NovaTreeNode +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit, + C2: StepCircuit, +{ + /// Creates a tree node which proves one computation and runs a base case F' proof. The running instances + /// are set to defaults and the new proofs are set ot this base case proof. + pub fn new( + pp: &PublicParams, + c_primary: C1, + c_secondary: C2, + i: u64, + z_start_primary: Vec, + z_end_primary: Vec, + z_start_secondary: Vec, + z_end_secondary: Vec, + ) -> Result { + // base case for the primary + let mut cs_primary: SatisfyingAssignment = SatisfyingAssignment::new(); + let inputs_primary: NovaAugmentedParallelCircuitInputs = + NovaAugmentedParallelCircuitInputs::new( + pp.r1cs_shape_secondary.digest(), + G1::Scalar::from(i), + G1::Scalar::from(i), + G1::Scalar::from(i + 1), + G1::Scalar::from(i + 1), + z_start_primary.clone(), + z_start_primary.clone(), + z_end_primary.clone(), + z_end_primary.clone(), + None, + None, + None, + None, + None, + None, + None, + ); + + let circuit_primary: NovaAugmentedParallelCircuit = NovaAugmentedParallelCircuit::new( + pp.augmented_circuit_params_primary.clone(), + Some(inputs_primary), + c_primary.clone(), + pp.ro_consts_circuit_primary.clone(), + ); + let _ = circuit_primary.synthesize(&mut cs_primary); + let (u_primary, w_primary) = cs_primary + .r1cs_instance_and_witness(&pp.r1cs_shape_primary, &pp.ck_primary) + .map_err(|_e| NovaError::UnSat)?; + + // base case for the secondary + let mut cs_secondary: SatisfyingAssignment = SatisfyingAssignment::new(); + + let inputs_secondary: NovaAugmentedParallelCircuitInputs = + NovaAugmentedParallelCircuitInputs::new( + pp.r1cs_shape_primary.digest(), + G2::Scalar::from(i), + G2::Scalar::from(i), + G2::Scalar::from(i + 1), + G2::Scalar::from(i + 1), + z_start_secondary.clone(), + z_start_secondary.clone(), + z_end_secondary.clone(), + z_end_secondary.clone(), + None, + None, + None, + None, + None, + None, + None, + ); + let circuit_secondary: NovaAugmentedParallelCircuit = NovaAugmentedParallelCircuit::new( + pp.augmented_circuit_params_secondary.clone(), + Some(inputs_secondary), + c_secondary.clone(), + pp.ro_consts_circuit_secondary.clone(), + ); + let _ = circuit_secondary.synthesize(&mut cs_secondary); + let (u_secondary, w_secondary) = cs_secondary + .r1cs_instance_and_witness(&pp.r1cs_shape_secondary, &pp.ck_secondary) + .map_err(|_e| NovaError::UnSat)?; + + // IVC proof for the primary circuit + let w_primary = RelaxedR1CSWitness::from_r1cs_witness(&pp.r1cs_shape_primary, &w_primary); + let u_primary = + RelaxedR1CSInstance::from_r1cs_instance(&pp.ck_primary, &pp.r1cs_shape_primary, &u_primary); + let W_primary = w_primary.clone(); + let U_primary = u_primary.clone(); + + // IVC proof of the secondary circuit + let w_secondary = + RelaxedR1CSWitness::::from_r1cs_witness(&pp.r1cs_shape_secondary, &w_secondary); + let u_secondary = RelaxedR1CSInstance::::from_r1cs_instance( + &pp.ck_secondary, + &pp.r1cs_shape_secondary, + &u_secondary, + ); + let W_secondary = w_secondary.clone(); + let U_secondary = u_secondary.clone(); + + if z_start_primary.len() != pp.F_arity_primary + || z_start_secondary.len() != pp.F_arity_secondary + { + return Err(NovaError::InvalidStepOutputLength); + } + + let i_start = i; + let i_end = i + 1; + + Ok(Self { + W_primary, + U_primary, + w_primary, + u_primary, + W_secondary, + U_secondary, + w_secondary, + u_secondary, + i_start, + i_end, + z_start_primary, + z_end_primary, + z_start_secondary, + z_end_secondary, + _p_c1: Default::default(), + _p_c2: Default::default(), + }) + } + + /// Merges another node into this node. The node this is called on is treated as the left node and the node which is + /// consumed is treated as the right node. + pub fn merge( + self, + right: NovaTreeNode, + pp: &PublicParams, + c_primary: &C1, + c_secondary: &C2, + ) -> Result { + // We have to merge two proofs where the right starts one index after the left ends + // note that this would fail in the proof step but we error earlier here for debugging clarity. + if self.i_end + 1 != right.i_start { + return Err(NovaError::InvalidNodeMerge); + } + + // First we fold the secondary instances of both the left and right children in the secondary curve + let (nifs_left_secondary, (left_U_secondary, left_W_secondary)) = NIFS::prove( + &pp.ck_secondary, + &pp.ro_consts_secondary, + &scalar_as_base::(pp.digest()), + &pp.r1cs_shape_secondary, + &self.U_secondary, + &self.W_secondary, + &self.u_secondary, + &self.w_secondary, + false, + )?; + let (nifs_right_secondary, (right_U_secondary, right_W_secondary)) = NIFS::prove( + &pp.ck_secondary, + &pp.ro_consts_secondary, + &scalar_as_base::(pp.digest()), + &pp.r1cs_shape_secondary, + &right.U_secondary, + &right.W_secondary, + &right.u_secondary, + &right.w_secondary, + false, + )?; + let (nifs_secondary, (U_secondary, W_secondary)) = NIFS::prove( + &pp.ck_secondary, + &pp.ro_consts_secondary, + &scalar_as_base::(pp.digest()), + &pp.r1cs_shape_secondary, + &left_U_secondary, + &left_W_secondary, + &right_U_secondary, + &right_W_secondary, + true, + )?; + + // Next we construct a proof of this folding and of the invocation of F + + let mut cs_primary: SatisfyingAssignment = SatisfyingAssignment::new(); + + let inputs_primary: NovaAugmentedParallelCircuitInputs = + NovaAugmentedParallelCircuitInputs::new( + pp.r1cs_shape_secondary.digest(), + G1::Scalar::from(self.i_start), + G1::Scalar::from(self.i_end), + G1::Scalar::from(right.i_start), + G1::Scalar::from(right.i_end), + self.z_start_primary.clone(), + self.z_end_primary, + right.z_start_primary, + right.z_end_primary.clone(), + Some(self.U_secondary), + Some(self.u_secondary), + Some(right.U_secondary), + Some(right.u_secondary), + Some(Commitment::::decompress(&nifs_left_secondary.comm_T)?), + Some(Commitment::::decompress(&nifs_right_secondary.comm_T)?), + Some(Commitment::::decompress(&nifs_secondary.comm_T)?), + ); + + let circuit_primary: NovaAugmentedParallelCircuit = NovaAugmentedParallelCircuit::new( + pp.augmented_circuit_params_primary.clone(), + Some(inputs_primary), + c_primary.clone(), + pp.ro_consts_circuit_primary.clone(), + ); + let _ = circuit_primary.synthesize(&mut cs_primary); + + let (u_primary, w_primary) = cs_primary + .r1cs_instance_and_witness(&pp.r1cs_shape_primary, &pp.ck_primary) + .map_err(|_e| NovaError::UnSat)?; + + let u_primary = + RelaxedR1CSInstance::from_r1cs_instance(&pp.ck_primary, &pp.r1cs_shape_primary, &u_primary); + let w_primary = RelaxedR1CSWitness::from_r1cs_witness(&pp.r1cs_shape_primary, &w_primary); + + // Now we fold the instances of the primary proof + let (nifs_left_primary, (left_U_primary, left_W_primary)) = NIFS::prove( + &pp.ck_primary, + &pp.ro_consts_primary, + &pp.digest(), + &pp.r1cs_shape_primary, + &self.U_primary, + &self.W_primary, + &self.u_primary, + &self.w_primary, + false, + )?; + let (nifs_right_primary, (right_U_primary, right_W_primary)) = NIFS::prove( + &pp.ck_primary, + &pp.ro_consts_primary, + &pp.digest(), + &pp.r1cs_shape_primary, + &right.U_primary, + &right.W_primary, + &right.u_primary, + &right.w_primary, + false, + )?; + let (nifs_primary, (U_primary, W_primary)) = NIFS::prove( + &pp.ck_primary, + &pp.ro_consts_primary, + &pp.digest(), + &pp.r1cs_shape_primary, + &left_U_primary, + &left_W_primary, + &right_U_primary, + &right_W_primary, + true, + )?; + + // Next we construct a proof of this folding in the secondary curve + let mut cs_secondary: SatisfyingAssignment = SatisfyingAssignment::new(); + + let inputs_secondary: NovaAugmentedParallelCircuitInputs = + NovaAugmentedParallelCircuitInputs::::new( + pp.r1cs_shape_primary.digest(), + G2::Scalar::from(self.i_start), + G2::Scalar::from(self.i_end), + G2::Scalar::from(right.i_start), + G2::Scalar::from(right.i_end), + self.z_start_secondary.clone(), + self.z_end_secondary, + right.z_start_secondary, + right.z_end_secondary.clone(), + Some(self.U_primary), + Some(self.u_primary), + Some(right.U_primary), + Some(right.u_primary), + Some(Commitment::::decompress(&nifs_left_primary.comm_T)?), + Some(Commitment::::decompress(&nifs_right_primary.comm_T)?), + Some(Commitment::::decompress(&nifs_primary.comm_T)?), + ); + + let circuit_secondary: NovaAugmentedParallelCircuit = NovaAugmentedParallelCircuit::new( + pp.augmented_circuit_params_secondary.clone(), + Some(inputs_secondary), + c_secondary.clone(), + pp.ro_consts_circuit_secondary.clone(), + ); + let _ = circuit_secondary.synthesize(&mut cs_secondary); + + let (u_secondary, w_secondary) = cs_secondary + .r1cs_instance_and_witness(&pp.r1cs_shape_secondary, &pp.ck_secondary) + .map_err(|_e| NovaError::UnSat)?; + + // Give these a trivial error vector + let u_secondary = RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_secondary, + &pp.r1cs_shape_secondary, + &u_secondary, + ); + let w_secondary = RelaxedR1CSWitness::from_r1cs_witness(&pp.r1cs_shape_secondary, &w_secondary); + + // Name each of these to match struct fields + let i_start = self.i_start; + let i_end = right.i_end; + let z_start_primary = self.z_start_primary; + let z_end_primary = right.z_end_primary; + let z_start_secondary = self.z_start_secondary; + let z_end_secondary = right.z_end_secondary; + + Ok(Self { + // Primary running instance + W_primary, + U_primary, + // Primary new instance + w_primary, + u_primary, + // The running instance of the secondary + W_secondary, + U_secondary, + // The running instance of the secondary + w_secondary, + u_secondary, + // The range data + i_start, + i_end, + z_start_primary, + z_end_primary, + z_start_secondary, + z_end_secondary, + _p_c1: Default::default(), + _p_c2: Default::default(), + }) + } +} + +/// Structure for parallelization +#[derive(Debug, Clone)] +pub struct ParallelSNARK +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit, + C2: StepCircuit, +{ + nodes: Vec>, +} + +/// Implementation for parallelization SNARK +impl ParallelSNARK +where + G1: Group::Scalar>, + G2: Group::Scalar>, + C1: StepCircuit + OutputStepCircuit, + C2: StepCircuit + OutputStepCircuit, +{ + /// Create a new instance of parallel SNARK + pub fn new( + pp: &PublicParams, + steps: usize, + z0_primary: Vec, + z0_secondary: Vec, + c_primary: C1, + c_secondary: C2, + ) -> Self { + // Tuple's structure is (index, zi_primary, zi_secondary) + let mut zi = Vec::<(usize, Vec, Vec)>::new(); + // First input value of Z0, these steps can't be done in parallel + zi.push((0, z0_primary.clone(), z0_secondary.clone())); + for i in 1..steps { + let (index, prev_primary, prev_secondary) = &zi[i - 1]; + zi.push(( + i, + c_primary.output(prev_primary), + c_secondary.output(prev_secondary), + )); + } + // Do calculate node tree in parallel + let nodes = zi + .par_chunks(2) + .map(|item| { + match item { + // There are 2 nodes + [l, r] => NovaTreeNode::new( + pp, + c_primary.clone(), + c_secondary.clone(), + l.0 as u64, + l.1.clone(), + r.1.clone(), + l.2.clone(), + r.2.clone(), + ) + .expect("Unable to create base node"), + // Just 1 node left + [l] => NovaTreeNode::new( + pp, + c_primary.clone(), + c_secondary.clone(), + l.0 as u64, + zi[l.0 - 1].1.clone(), + l.1.clone(), + zi[l.0 - 1].2.clone(), + l.2.clone(), + ) + .expect("Unable to create the last base node"), + _ => panic!("Unexpected chunk size"), + } + }) + .collect(); + // Create a new parallel prover wit basic leafs + Self { nodes } + } + + /// Perform the proving in parallel + pub fn prove(&mut self, pp: &PublicParams, c_primary: &C1, c_secondary: &C2) { + // Calculate the max height of the tree + // ⌈log2(n)⌉ + 1 + let max_height = ((self.nodes.len() as f64).log2().ceil() + 1f64) as usize; + + // Build up the tree with max given height + for level in 0..max_height { + // Exist if we on the root of the tree + if self.nodes.len() == 1 { + break; + } + // New nodes list will reduce a half each round + self.nodes = self + .nodes + .par_chunks(2) + .map(|item| match item { + // There are 2 nodes in the chunk + [vl, vr] => (*vl) + .clone() + .merge((*vr).clone(), pp, c_primary, c_secondary) + .expect("Merge the left and right should work"), + // Just 1 node left, we carry it to the next level + [vl] => (*vl).clone(), + _ => panic!("Invalid chunk size"), + }) + .collect(); + } + } + + /// Get all nodes from given instance + pub fn get_nodes(&self) -> Vec> { + self.nodes.clone() + } + + /// Get current length of current level + pub fn get_tree_size(&self) -> usize { + self.nodes.len() + } +} + +mod tests { + use super::*; + type G1 = pasta_curves::pallas::Point; + type G2 = pasta_curves::vesta::Point; + use crate::traits::circuit::{OutputStepCircuit, TrivialCircuit}; + use bellpepper::gadgets::num::AllocatedNum; + use bellpepper_core::{ConstraintSystem, SynthesisError}; + use core::marker::PhantomData; + use ff::PrimeField; + + #[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 = AllocatedNum::alloc(cs.namespace(|| "y"), || { + Ok(x_cu.get_value().unwrap() + x.get_value().unwrap() + F::from(5u64)) + })?; + + 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]) + } + + // fn output(&self, z: &[F]) -> Vec { + // vec![z[0] * z[0] * z[0] + z[0] + F::from(5u64)] + // } + } + + impl OutputStepCircuit for CubicCircuit + where + F: PrimeField, + { + fn output(&self, z: &[F]) -> Vec { + let x = &z[0]; + let x_sq = x.square(); + let x_cu = *x * x_sq; + vec![x_cu] + } + } + + #[test] + fn test_parallel_ivc_base() { + // produce public parameters + let pp = PublicParams::< + G1, + G2, + TrivialCircuit<::Scalar>, + CubicCircuit<::Scalar>, + >::setup( + TrivialCircuit::default(), + CubicCircuit::default(), + None, + None, + ); + + let num_steps = 1; + + // produce a recursive SNARK + let res = NovaTreeNode::new( + &pp, + TrivialCircuit::default(), + CubicCircuit::default(), + 0, + vec![::Scalar::one()], + vec![::Scalar::one()], + vec![::Scalar::one()], + vec![::Scalar::from(5u64)], + ); + assert!(res.is_ok()); + let recursive_snark = res.unwrap(); + } + + #[test] + fn test_parallel_combine_two_ivc() { + // produce public parameters + let pp = PublicParams::< + G1, + G2, + TrivialCircuit<::Scalar>, + CubicCircuit<::Scalar>, + >::setup( + TrivialCircuit::default(), + CubicCircuit::default(), + None, + None, + ); + + // produce a recursive SNARK + let res_0 = NovaTreeNode::new( + &pp, + TrivialCircuit::default(), + CubicCircuit::default(), + 0, + vec![::Scalar::one()], + vec![::Scalar::one()], + vec![::Scalar::zero()], + vec![::Scalar::from(5u64)], + ); + assert!(res_0.is_ok()); + let recursive_snark_0 = res_0.unwrap(); + + let res_1 = NovaTreeNode::new( + &pp, + TrivialCircuit::default(), + CubicCircuit::default(), + 2, + vec![::Scalar::one()], + vec![::Scalar::one()], + vec![::Scalar::from(135u64)], + vec![::Scalar::from(2460515u64)], + ); + assert!(res_1.is_ok()); + let recursive_snark = res_1.unwrap(); + + let res_2 = recursive_snark_0.merge( + recursive_snark, + &pp, + &TrivialCircuit::default(), + &CubicCircuit::default(), + ); + assert!(res_2.is_ok()); + } +} diff --git a/src/r1cs/mod.rs b/src/r1cs/mod.rs index 83b260dd9..99656e34a 100644 --- a/src/r1cs/mod.rs +++ b/src/r1cs/mod.rs @@ -288,8 +288,8 @@ impl R1CSShape { ck: &CommitmentKey, U1: &RelaxedR1CSInstance, W1: &RelaxedR1CSWitness, - U2: &R1CSInstance, - W2: &R1CSWitness, + U2: &RelaxedR1CSInstance, + W2: &RelaxedR1CSWitness, ) -> Result<(Vec, Commitment), NovaError> { let (AZ_1, BZ_1, CZ_1) = tracing::trace_span!("AZ_1, BZ_1, CZ_1").in_scope(|| { let Z1 = [W1.W.clone(), vec![U1.u], U1.X.clone()].concat(); @@ -464,15 +464,15 @@ impl RelaxedR1CSWitness { (CE::::commit(ck, &self.W), CE::::commit(ck, &self.E)) } - /// Folds an incoming `R1CSWitness` into the current one + /// Folds an incoming `RelaxedR1CSWitness` into the current one pub fn fold( &self, - W2: &R1CSWitness, + W2: &RelaxedR1CSWitness, T: &[G::Scalar], r: &G::Scalar, ) -> Result, NovaError> { let (W1, E1) = (&self.W, &self.E); - let W2 = &W2.W; + let (W2, E2) = (&W2.W, &W2.E); if W1.len() != W2.len() { return Err(NovaError::InvalidWitnessLength); @@ -483,10 +483,12 @@ impl RelaxedR1CSWitness { .zip(W2) .map(|(a, b)| *a + *r * *b) .collect::>(); + let r_squared = *r * *r; let E = E1 .par_iter() .zip(T) - .map(|(a, b)| *a + *r * *b) + .zip(E2) + .map(|((a, b), c)| *a + *r * *b + r_squared * *c) .collect::>(); Ok(RelaxedR1CSWitness { W, E }) } @@ -544,13 +546,13 @@ impl RelaxedR1CSInstance { /// Folds an incoming `RelaxedR1CSInstance` into the current one pub fn fold( &self, - U2: &R1CSInstance, + U2: &RelaxedR1CSInstance, comm_T: &Commitment, r: &G::Scalar, ) -> Result, NovaError> { let (X1, u1, comm_W_1, comm_E_1) = (&self.X, &self.u, &self.comm_W.clone(), &self.comm_E.clone()); - let (X2, comm_W_2) = (&U2.X, &U2.comm_W); + let (X2, u2, comm_W_2, comm_E_2) = (&U2.X, &U2.u, &U2.comm_W, &U2.comm_E.clone()); // weighted sum of X, comm_W, comm_E, and u let X = X1 @@ -559,8 +561,8 @@ impl RelaxedR1CSInstance { .map(|(a, b)| *a + *r * *b) .collect::>(); let comm_W = *comm_W_1 + *comm_W_2 * *r; - let comm_E = *comm_E_1 + *comm_T * *r; - let u = *u1 + *r; + let comm_E = *comm_E_1 + *comm_T * *r + *comm_E_2 * *r * *r; + let u = *u1 + *r * *u2; Ok(RelaxedR1CSInstance { comm_W, @@ -599,6 +601,16 @@ impl AbsorbInROTrait for RelaxedR1CSInstance { } } +impl RelaxedR1CSInstance { + /// absorb as though this were a strict R1CSInstance + pub fn absorb_in_ro_as_strict(&self, ro: &mut G::RO) { + // TODO: add assertions checking E and u. + self.comm_W.absorb_in_ro(ro); + for x in &self.X { + ro.absorb(scalar_as_base::(*x)); + } + } +} #[cfg(test)] mod tests { use ff::Field; diff --git a/src/supernova/circuit.rs b/src/supernova/circuit.rs index b3fc825df..639db6ec1 100644 --- a/src/supernova/circuit.rs +++ b/src/supernova/circuit.rs @@ -26,7 +26,7 @@ use crate::{ le_bits_to_num, scalar_as_base, }, }, - r1cs::{R1CSInstance, RelaxedR1CSInstance}, + r1cs::RelaxedR1CSInstance, traits::{ circuit_supernova::EnforcingStepCircuit, commitment::CommitmentTrait, Group, ROCircuitTrait, ROConstantsCircuit, @@ -75,7 +75,7 @@ pub struct SuperNovaAugmentedCircuitInputs<'a, G: Group> { z0: &'a [G::Base], zi: Option<&'a [G::Base]>, U: Option<&'a [Option>]>, - u: Option<&'a R1CSInstance>, + u: Option<&'a RelaxedR1CSInstance>, T: Option<&'a Commitment>, program_counter: Option, last_augmented_circuit_index: G::Base, @@ -89,7 +89,7 @@ impl<'a, G: Group> SuperNovaAugmentedCircuitInputs<'a, G> { z0: &'a [G::Base], zi: Option<&'a [G::Base]>, U: Option<&'a [Option>]>, - u: Option<&'a R1CSInstance>, + u: Option<&'a RelaxedR1CSInstance>, T: Option<&'a Commitment>, program_counter: Option, last_augmented_circuit_index: G::Base, diff --git a/src/supernova/mod.rs b/src/supernova/mod.rs index 78eb5086c..f83e2f6a4 100644 --- a/src/supernova/mod.rs +++ b/src/supernova/mod.rs @@ -447,6 +447,11 @@ where // base case for the secondary let mut cs_secondary: SatisfyingAssignment = SatisfyingAssignment::new(); + let relaxed_u_primary = RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_primary, + &pp[circuit_index].r1cs_shape, + &u_primary, + ); let inputs_secondary: SuperNovaAugmentedCircuitInputs<'_, G1> = SuperNovaAugmentedCircuitInputs::new( pp.digest(), @@ -454,7 +459,7 @@ where z0_secondary, None, None, - Some(&u_primary), + Some(&relaxed_u_primary), None, None, G2::Scalar::from(circuit_index as u64), @@ -576,8 +581,16 @@ where &pp.circuit_shape_secondary.r1cs_shape, self.r_U_secondary[0].as_ref().unwrap(), self.r_W_secondary[0].as_ref().unwrap(), - &self.l_u_secondary, - &self.l_w_secondary, + &RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_secondary, + &pp.circuit_shape_secondary.r1cs_shape, + &self.l_u_secondary, + ), + &RelaxedR1CSWitness::from_r1cs_witness( + &pp.circuit_shape_secondary.r1cs_shape, + &self.l_w_secondary, + ), + false, ) .map_err(SuperNovaError::NovaError)?; @@ -588,6 +601,12 @@ where let mut cs_primary: SatisfyingAssignment = SatisfyingAssignment::new(); let T = Commitment::::decompress(&nifs_secondary.comm_T).map_err(SuperNovaError::NovaError)?; + + let relaxed_l_u_secondary = RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_secondary, + &pp.circuit_shape_secondary.r1cs_shape, + &self.l_u_secondary, + ); let inputs_primary: SuperNovaAugmentedCircuitInputs<'_, G2> = SuperNovaAugmentedCircuitInputs::new( scalar_as_base::(self.pp_digest), @@ -595,7 +614,7 @@ where z0_primary, Some(&self.zi_primary), Some(&self.r_U_secondary), - Some(&self.l_u_secondary), + Some(&relaxed_l_u_secondary), Some(&T), Some(self.program_counter), G1::Scalar::ZERO, @@ -635,8 +654,13 @@ where &pp[circuit_index].r1cs_shape, r_U_primary, r_W_primary, - &l_u_primary, - &l_w_primary, + &RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_primary, + &pp[circuit_index].r1cs_shape, + &l_u_primary, + ), + &RelaxedR1CSWitness::from_r1cs_witness(&pp[circuit_index].r1cs_shape, &l_w_primary), + false, ) .map_err(SuperNovaError::NovaError)?, _ => NIFS::prove( @@ -646,8 +670,13 @@ where &pp[circuit_index].r1cs_shape, &RelaxedR1CSInstance::default(&pp.ck_primary, &pp[circuit_index].r1cs_shape), &RelaxedR1CSWitness::default(&pp[circuit_index].r1cs_shape), - &l_u_primary, - &l_w_primary, + &RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_primary, + &pp[circuit_index].r1cs_shape, + &l_u_primary, + ), + &RelaxedR1CSWitness::from_r1cs_witness(&pp[circuit_index].r1cs_shape, &l_w_primary), + false, ) .map_err(SuperNovaError::NovaError)?, }; @@ -655,6 +684,12 @@ where let mut cs_secondary: SatisfyingAssignment = SatisfyingAssignment::new(); let binding = Commitment::::decompress(&nifs_primary.comm_T).map_err(SuperNovaError::NovaError)?; + + let relaxed_l_u_primary = RelaxedR1CSInstance::from_r1cs_instance( + &pp.ck_primary, + &pp[circuit_index].r1cs_shape, + &l_u_primary, + ); let inputs_secondary: SuperNovaAugmentedCircuitInputs<'_, G1> = SuperNovaAugmentedCircuitInputs::new( self.pp_digest, @@ -662,7 +697,7 @@ where z0_secondary, Some(&self.zi_secondary), Some(&self.r_U_primary), - Some(&l_u_primary), + Some(&relaxed_l_u_primary), Some(&binding), None, G2::Scalar::from(circuit_index as u64), diff --git a/src/supernova/test.rs b/src/supernova/test.rs index d60713cfb..533a90ff6 100644 --- a/src/supernova/test.rs +++ b/src/supernova/test.rs @@ -619,6 +619,8 @@ fn test_recursive_circuit_with( // Make sure that this is satisfiable assert!(shape1.is_sat(&ck1, &inst1, &witness1).is_ok()); + let inst1 = RelaxedR1CSInstance::from_r1cs_instance(&ck1, &shape1, &inst1); + // Execute the base case for the secondary let zero2 = <::Base as Field>::ZERO; let z0 = vec![zero2; arity2]; diff --git a/src/traits/circuit.rs b/src/traits/circuit.rs index d2cd2aaee..c7874a359 100644 --- a/src/traits/circuit.rs +++ b/src/traits/circuit.rs @@ -20,6 +20,12 @@ pub trait StepCircuit: Send + Sync + Clone { ) -> Result>, SynthesisError>; } +/// A trait for circuits that can explicitly compute their output, to aid in generating parallelizable execution traces. +pub trait OutputStepCircuit { + /// return the output of the step when provided with the step's input + fn output(&self, z: &[F]) -> Vec; +} + /// A trivial step circuit that simply returns the input #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TrivialCircuit { @@ -42,3 +48,9 @@ where Ok(z.to_vec()) } } + +impl OutputStepCircuit for TrivialCircuit { + fn output(&self, z: &[F]) -> Vec { + z.to_vec() + } +}