diff --git a/Cargo.toml b/Cargo.toml index b33efebf..05fa47dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ byteorder = "1.4.3" thiserror = "1.0" halo2curves = { version = "0.4.0", features = ["derive_serde"] } group = "0.13.0" +log = "0.4.17" [target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] pasta-msm = { version = "0.1.4" } @@ -68,6 +69,11 @@ harness = false name = "sha256" harness = false +[[bench]] +name = "recursive-snark-supernova" +harness = false +required-features = ["supernova"] + [features] default = [] # Compiles in portable mode, w/o ISA extensions => binary can be executed on all systems. @@ -75,3 +81,4 @@ portable = ["pasta-msm/portable"] cuda = ["neptune/cuda", "neptune/pasta", "neptune/arity24"] opencl = ["neptune/opencl", "neptune/pasta", "neptune/arity24"] flamegraph = ["pprof/flamegraph", "pprof/criterion"] +supernova = [] diff --git a/benches/recursive-snark-supernova.rs b/benches/recursive-snark-supernova.rs new file mode 100644 index 00000000..40329069 --- /dev/null +++ b/benches/recursive-snark-supernova.rs @@ -0,0 +1,329 @@ +#![allow(non_snake_case)] + +use bellpepper_core::{num::AllocatedNum, ConstraintSystem, SynthesisError}; +use core::marker::PhantomData; +use criterion::*; +use ff::PrimeField; +use nova_snark::{ + compute_digest, + supernova::RecursiveSNARK, + supernova::{gen_commitmentkey_by_r1cs, PublicParams, RunningClaim}, + traits::{ + circuit_supernova::{StepCircuit, TrivialTestCircuit}, + Group, + }, +}; +use std::time::Duration; + +type G1 = pasta_curves::pallas::Point; +type G2 = pasta_curves::vesta::Point; + +// To run these benchmarks, first download `criterion` with `cargo install cargo-criterion`. +// Then `cargo criterion --bench recursive-snark-supernova`. The results are located in `target/criterion/data/`. +// For flamegraphs, run `cargo criterion --bench recursive-snark-supernova --features flamegraph -- --profile-time `. +// The results are located in `target/criterion/profile/`. +cfg_if::cfg_if! { + if #[cfg(feature = "flamegraph")] { + criterion_group! { + name = recursive_snark_supernova; + config = Criterion::default().warm_up_time(Duration::from_millis(3000)).with_profiler(pprof::criterion::PProfProfiler::new(100, pprof::criterion::Output::Flamegraph(None))); + targets = bench_one_augmented_circuit_recursive_snark, bench_two_augmented_circuit_recursive_snark + } + } else { + criterion_group! { + name = recursive_snark_supernova; + config = Criterion::default().warm_up_time(Duration::from_millis(3000)); + targets = bench_one_augmented_circuit_recursive_snark, bench_two_augmented_circuit_recursive_snark + } + } +} + +criterion_main!(recursive_snark_supernova); + +fn bench_one_augmented_circuit_recursive_snark(c: &mut Criterion) { + let num_cons_verifier_circuit_primary = 9819; + // we vary the number of constraints in the step circuit + for &num_cons_in_augmented_circuit in + [9819, 16384, 32768, 65536, 131072, 262144, 524288, 1048576].iter() + { + // number of constraints in the step circuit + let num_cons = num_cons_in_augmented_circuit - num_cons_verifier_circuit_primary; + + let mut group = c.benchmark_group(format!( + "RecursiveSNARKSuperNova-1circuit-StepCircuitSize-{num_cons}" + )); + group.sample_size(10); + + let c_primary = NonTrivialTestCircuit::new(num_cons); + let c_secondary = TrivialTestCircuit::default(); + + // Structuring running claims + let mut running_claim1 = RunningClaim::< + G1, + G2, + NonTrivialTestCircuit<::Scalar>, + TrivialTestCircuit<::Scalar>, + >::new(0, c_primary, c_secondary.clone(), 1); + + let (r1cs_shape_primary, r1cs_shape_secondary) = running_claim1.get_r1cs_shape(); + let ck_primary = gen_commitmentkey_by_r1cs(r1cs_shape_primary); + let ck_secondary = gen_commitmentkey_by_r1cs(r1cs_shape_secondary); + + // set unified ck_primary, ck_secondary and update digest + running_claim1.set_commitmentkey(ck_primary.clone(), ck_secondary.clone()); + let digest = compute_digest::>(&[running_claim1.get_publicparams()]); + + // Bench time to produce a recursive SNARK; + // we execute a certain number of warm-up steps since executing + // the first step is cheaper than other steps owing to the presence of + // a lot of zeros in the satisfying assignment + let num_warmup_steps = 10; + let z0_primary = vec![::Scalar::from(2u64)]; + let z0_secondary = vec![::Scalar::from(2u64)]; + let initial_program_counter = ::Scalar::from(0); + let mut recursive_snark_option: Option> = None; + + for _ in 0..num_warmup_steps { + let program_counter = recursive_snark_option + .as_ref() + .map(|recursive_snark| recursive_snark.get_program_counter()) + .unwrap_or_else(|| initial_program_counter); + + let mut recursive_snark = recursive_snark_option.unwrap_or_else(|| { + RecursiveSNARK::iter_base_step( + &running_claim1, + digest, + program_counter, + 0, + 1, + &z0_primary, + &z0_secondary, + ) + .unwrap() + }); + + let res = recursive_snark.prove_step(&running_claim1, &z0_primary, &z0_secondary); + if let Err(e) = &res { + println!("res failed {:?}", e); + } + assert!(res.is_ok()); + let res = recursive_snark.verify(&running_claim1, &z0_primary, &z0_secondary); + if let Err(e) = &res { + println!("res failed {:?}", e); + } + assert!(res.is_ok()); + recursive_snark_option = Some(recursive_snark) + } + + assert!(recursive_snark_option.is_some()); + let recursive_snark = recursive_snark_option.unwrap(); + + // Benchmark the prove time + group.bench_function("Prove", |b| { + b.iter(|| { + // produce a recursive SNARK for a step of the recursion + assert!(black_box(&mut recursive_snark.clone()) + .prove_step( + black_box(&running_claim1), + black_box(&[::Scalar::from(2u64)]), + black_box(&[::Scalar::from(2u64)]), + ) + .is_ok()); + }) + }); + + // Benchmark the verification time + group.bench_function("Verify", |b| { + b.iter(|| { + assert!(black_box(&mut recursive_snark.clone()) + .verify( + black_box(&running_claim1), + black_box(&[::Scalar::from(2u64)]), + black_box(&[::Scalar::from(2u64)]), + ) + .is_ok()); + }); + }); + group.finish(); + } +} + +fn bench_two_augmented_circuit_recursive_snark(c: &mut Criterion) { + let num_cons_verifier_circuit_primary = 9819; + // we vary the number of constraints in the step circuit + for &num_cons_in_augmented_circuit in + [9819, 16384, 32768, 65536, 131072, 262144, 524288, 1048576].iter() + { + // number of constraints in the step circuit + let num_cons = num_cons_in_augmented_circuit - num_cons_verifier_circuit_primary; + + let mut group = c.benchmark_group(format!( + "RecursiveSNARKSuperNova-2circuit-StepCircuitSize-{num_cons}" + )); + group.sample_size(10); + + let c_primary = NonTrivialTestCircuit::new(num_cons); + let c_secondary = TrivialTestCircuit::default(); + + // Structuring running claims + let mut running_claim1 = RunningClaim::< + G1, + G2, + NonTrivialTestCircuit<::Scalar>, + TrivialTestCircuit<::Scalar>, + >::new(0, c_primary.clone(), c_secondary.clone(), 2); + + // Structuring running claims + let mut running_claim2 = RunningClaim::< + G1, + G2, + NonTrivialTestCircuit<::Scalar>, + TrivialTestCircuit<::Scalar>, + >::new(1, c_primary, c_secondary.clone(), 2); + + let (r1cs_shape_primary, r1cs_shape_secondary) = running_claim1.get_r1cs_shape(); + let ck_primary = gen_commitmentkey_by_r1cs(r1cs_shape_primary); + let ck_secondary = gen_commitmentkey_by_r1cs(r1cs_shape_secondary); + + // set unified ck_primary, ck_secondary and update digest + running_claim1.set_commitmentkey(ck_primary.clone(), ck_secondary.clone()); + running_claim2.set_commitmentkey(ck_primary.clone(), ck_secondary.clone()); + + let digest = compute_digest::>(&[ + running_claim1.get_publicparams(), + running_claim2.get_publicparams(), + ]); + + // Bench time to produce a recursive SNARK; + // we execute a certain number of warm-up steps since executing + // the first step is cheaper than other steps owing to the presence of + // a lot of zeros in the satisfying assignment + let num_warmup_steps = 10; + let z0_primary = vec![::Scalar::from(2u64)]; + let z0_secondary = vec![::Scalar::from(2u64)]; + let initial_program_counter = ::Scalar::from(0); + let mut recursive_snark_option: Option> = None; + let mut selected_augmented_circuit = 0; + + for _ in 0..num_warmup_steps { + let program_counter = recursive_snark_option + .as_ref() + .map(|recursive_snark| recursive_snark.get_program_counter()) + .unwrap_or_else(|| initial_program_counter); + + let mut recursive_snark = recursive_snark_option.unwrap_or_else(|| { + RecursiveSNARK::iter_base_step( + &running_claim1, + digest, + program_counter, + 0, + 2, + &z0_primary, + &z0_secondary, + ) + .unwrap() + }); + + if selected_augmented_circuit == 0 { + let res = recursive_snark.prove_step(&running_claim1, &z0_primary, &z0_secondary); + if let Err(e) = &res { + println!("res failed {:?}", e); + } + assert!(res.is_ok()); + let res = recursive_snark.verify(&running_claim1, &z0_primary, &z0_secondary); + if let Err(e) = &res { + println!("res failed {:?}", e); + } + assert!(res.is_ok()); + } else if selected_augmented_circuit == 1 { + let res = recursive_snark.prove_step(&running_claim2, &z0_primary, &z0_secondary); + if let Err(e) = &res { + println!("res failed {:?}", e); + } + assert!(res.is_ok()); + let res = recursive_snark.verify(&running_claim2, &z0_primary, &z0_secondary); + if let Err(e) = &res { + println!("res failed {:?}", e); + } + assert!(res.is_ok()); + } else { + unimplemented!() + } + selected_augmented_circuit = (selected_augmented_circuit + 1) % 2; + recursive_snark_option = Some(recursive_snark) + } + + assert!(recursive_snark_option.is_some()); + let recursive_snark = recursive_snark_option.unwrap(); + + // Benchmark the prove time + group.bench_function("Prove", |b| { + b.iter(|| { + // produce a recursive SNARK for a step of the recursion + assert!(black_box(&mut recursive_snark.clone()) + .prove_step( + black_box(&running_claim1), + black_box(&[::Scalar::from(2u64)]), + black_box(&[::Scalar::from(2u64)]), + ) + .is_ok()); + }) + }); + + // Benchmark the verification time + group.bench_function("Verify", |b| { + b.iter(|| { + assert!(black_box(&mut recursive_snark.clone()) + .verify( + black_box(&running_claim1), + black_box(&[::Scalar::from(2u64)]), + black_box(&[::Scalar::from(2u64)]), + ) + .is_ok()); + }); + }); + group.finish(); + } +} + +#[derive(Clone, Debug, Default)] +struct NonTrivialTestCircuit { + num_cons: usize, + _p: PhantomData, +} + +impl NonTrivialTestCircuit +where + F: PrimeField, +{ + pub fn new(num_cons: usize) -> Self { + Self { + num_cons, + _p: Default::default(), + } + } +} +impl StepCircuit for NonTrivialTestCircuit +where + F: PrimeField, +{ + fn arity(&self) -> usize { + 1 + } + + fn synthesize>( + &self, + cs: &mut CS, + pc: &AllocatedNum, + z: &[AllocatedNum], + ) -> Result<(AllocatedNum, Vec>), SynthesisError> { + // Consider a an equation: `x^2 = y`, where `x` and `y` are respectively the input and output. + let mut x = z[0].clone(); + let mut y = x.clone(); + for i in 0..self.num_cons { + y = x.square(cs.namespace(|| format!("x_sq_{i}")))?; + x = y.clone(); + } + Ok((pc.clone(), vec![y])) + } +} diff --git a/src/bellpepper/mod.rs b/src/bellpepper/mod.rs index 919544b9..718aa55f 100644 --- a/src/bellpepper/mod.rs +++ b/src/bellpepper/mod.rs @@ -50,7 +50,7 @@ mod tests { // First create the shape let mut cs: ShapeCS = ShapeCS::new(); let _ = synthesize_alloc_bit(&mut cs); - let (shape, ck) = cs.r1cs_shape(); + let (shape, ck) = cs.r1cs_shape_with_commitmentkey(); // Now get the assignment let mut cs: SatisfyingAssignment = SatisfyingAssignment::new(); diff --git a/src/bellpepper/r1cs.rs b/src/bellpepper/r1cs.rs index 7cb4f3c6..12b0ac1b 100644 --- a/src/bellpepper/r1cs.rs +++ b/src/bellpepper/r1cs.rs @@ -25,7 +25,14 @@ pub trait NovaWitness { /// `NovaShape` provides methods for acquiring `R1CSShape` and `CommitmentKey` from implementers. pub trait NovaShape { /// Return an appropriate `R1CSShape` and `CommitmentKey` structs. - fn r1cs_shape(&self) -> (R1CSShape, CommitmentKey); + fn r1cs_shape_with_commitmentkey(&self) -> (R1CSShape, CommitmentKey) { + let S = self.r1cs_shape(); + let ck = R1CS::::commitment_key(&S); + + (S, ck) + } + /// Return an appropriate `R1CSShape`. + fn r1cs_shape(&self) -> R1CSShape; } impl NovaWitness for SatisfyingAssignment { @@ -51,7 +58,7 @@ macro_rules! impl_nova_shape { where G::Scalar: PrimeField, { - fn r1cs_shape(&self) -> (R1CSShape, CommitmentKey) { + fn r1cs_shape(&self) -> R1CSShape { let mut A: Vec<(usize, usize, G::Scalar)> = Vec::new(); let mut B: Vec<(usize, usize, G::Scalar)> = Vec::new(); let mut C: Vec<(usize, usize, G::Scalar)> = Vec::new(); @@ -81,9 +88,7 @@ macro_rules! impl_nova_shape { res.unwrap() }; - let ck = R1CS::::commitment_key(&S); - - (S, ck) + S } } }; diff --git a/src/circuit.rs b/src/circuit.rs index 098e5793..da918cda 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -399,7 +399,7 @@ mod tests { NovaAugmentedCircuit::new(primary_params, None, &ttc1, ro_consts1.clone()); let mut cs: TestShapeCS = TestShapeCS::new(); let _ = circuit1.synthesize(&mut cs); - let (shape1, ck1) = cs.r1cs_shape(); + let (shape1, ck1) = cs.r1cs_shape_with_commitmentkey(); assert_eq!(cs.num_constraints(), num_constraints_primary); let ttc2 = TrivialTestCircuit::default(); @@ -408,7 +408,7 @@ mod tests { NovaAugmentedCircuit::new(secondary_params, None, &ttc2, ro_consts2.clone()); let mut cs: TestShapeCS = TestShapeCS::new(); let _ = circuit2.synthesize(&mut cs); - let (shape2, ck2) = cs.r1cs_shape(); + let (shape2, ck2) = cs.r1cs_shape_with_commitmentkey(); assert_eq!(cs.num_constraints(), num_constraints_secondary); // Execute the base case for the primary diff --git a/src/errors.rs b/src/errors.rs index 1cfc5c0e..3bb207fa 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -20,6 +20,9 @@ pub enum NovaError { /// returned if the supplied witness is not a satisfying witness to a given shape and instance #[error("UnSat")] UnSat, + /// returned if the supplied witness is not a satisfying witness to a given shape and instance, with error constraint index + #[error("UnSatIndex")] + UnSatIndex(usize), /// returned when the supplied compressed commitment cannot be decompressed #[error("DecompressionError")] DecompressionError, diff --git a/src/gadgets/ecc.rs b/src/gadgets/ecc.rs index b3848aa3..5dccef56 100644 --- a/src/gadgets/ecc.rs +++ b/src/gadgets/ecc.rs @@ -992,7 +992,7 @@ mod tests { let mut cs: TestShapeCS = TestShapeCS::new(); let _ = synthesize_smul::(cs.namespace(|| "synthesize")); println!("Number of constraints: {}", cs.num_constraints()); - let (shape, ck) = cs.r1cs_shape(); + let (shape, ck) = cs.r1cs_shape_with_commitmentkey(); // Then the satisfying assignment let mut cs: SatisfyingAssignment = SatisfyingAssignment::new(); @@ -1045,7 +1045,7 @@ mod tests { let mut cs: TestShapeCS = TestShapeCS::new(); let _ = synthesize_add_equal::(cs.namespace(|| "synthesize add equal")); println!("Number of constraints: {}", cs.num_constraints()); - let (shape, ck) = cs.r1cs_shape(); + let (shape, ck) = cs.r1cs_shape_with_commitmentkey(); // Then the satisfying assignment let mut cs: SatisfyingAssignment = SatisfyingAssignment::new(); @@ -1102,7 +1102,7 @@ mod tests { let mut cs: TestShapeCS = TestShapeCS::new(); let _ = synthesize_add_negation::(cs.namespace(|| "synthesize add equal")); println!("Number of constraints: {}", cs.num_constraints()); - let (shape, ck) = cs.r1cs_shape(); + let (shape, ck) = cs.r1cs_shape_with_commitmentkey(); // Then the satisfying assignment let mut cs: SatisfyingAssignment = SatisfyingAssignment::new(); diff --git a/src/gadgets/r1cs.rs b/src/gadgets/r1cs.rs index 456c1aad..4ab4ddf9 100644 --- a/src/gadgets/r1cs.rs +++ b/src/gadgets/r1cs.rs @@ -62,6 +62,7 @@ impl AllocatedR1CSInstance { } /// An Allocated Relaxed R1CS Instance +#[derive(Clone)] pub struct AllocatedRelaxedR1CSInstance { pub(crate) W: AllocatedPoint, pub(crate) E: AllocatedPoint, @@ -331,45 +332,108 @@ impl AllocatedRelaxedR1CSInstance { /// If the condition is true then returns this otherwise it returns the other pub fn conditionally_select::Base>>( &self, - mut cs: CS, + cs: CS, other: &AllocatedRelaxedR1CSInstance, condition: &Boolean, ) -> Result, SynthesisError> { - let W = AllocatedPoint::conditionally_select( - cs.namespace(|| "W = cond ? self.W : other.W"), - &self.W, - &other.W, - condition, - )?; + conditionally_select_alloc_relaxed_r1cs(cs, self, other, condition) + } +} - let E = AllocatedPoint::conditionally_select( - cs.namespace(|| "E = cond ? self.E : other.E"), - &self.E, - &other.E, +/// c = cond ? a: b, where a, b: AllocatedRelaxedR1CSInstance +pub fn conditionally_select_alloc_relaxed_r1cs< + G: Group, + CS: ConstraintSystem<::Base>, +>( + mut cs: CS, + a: &AllocatedRelaxedR1CSInstance, + b: &AllocatedRelaxedR1CSInstance, + condition: &Boolean, +) -> Result, SynthesisError> { + let c = AllocatedRelaxedR1CSInstance { + W: conditionally_select_point( + cs.namespace(|| "W = cond ? a.W : b.W"), + &a.W, + &b.W, condition, - )?; - - let u = conditionally_select( - cs.namespace(|| "u = cond ? self.u : other.u"), - &self.u, - &other.u, + )?, + E: conditionally_select_point( + cs.namespace(|| "E = cond ? a.E : b.E"), + &a.E, + &b.E, condition, - )?; - - let X0 = conditionally_select_bignat( - cs.namespace(|| "X[0] = cond ? self.X[0] : other.X[0]"), - &self.X0, - &other.X0, + )?, + u: conditionally_select( + cs.namespace(|| "u = cond ? a.u : b.u"), + &a.u, + &b.u, condition, - )?; - - let X1 = conditionally_select_bignat( - cs.namespace(|| "X[1] = cond ? self.X[1] : other.X[1]"), - &self.X1, - &other.X1, + )?, + X0: conditionally_select_bignat( + cs.namespace(|| "X[0] = cond ? a.X[0] : b.X[0]"), + &a.X0, + &b.X0, condition, - )?; + )?, + X1: conditionally_select_bignat( + cs.namespace(|| "X[1] = cond ? a.X[1] : b.X[1]"), + &a.X1, + &b.X1, + condition, + )?, + }; + Ok(c) +} - Ok(AllocatedRelaxedR1CSInstance { W, E, u, X0, X1 }) - } +/// c = cond ? a: b, where a, b: vec[AllocatedRelaxedR1CSInstance] +pub fn conditionally_select_vec_allocated_relaxed_r1cs_instance< + G: Group, + CS: ConstraintSystem<::Base>, +>( + mut cs: CS, + a: &[AllocatedRelaxedR1CSInstance], + b: &[AllocatedRelaxedR1CSInstance], + condition: &Boolean, +) -> Result>, SynthesisError> { + a.iter() + .enumerate() + .zip(b.iter()) + .map(|((i, a), b)| { + a.conditionally_select( + cs.namespace(|| format!("cond ? a[{}]: b[{}]", i, i)), + b, + condition, + ) + }) + .collect::>, _>>() +} + +/// c = cond ? a: b, where a, b: AllocatedPoint +pub fn conditionally_select_point::Base>>( + mut cs: CS, + a: &AllocatedPoint, + b: &AllocatedPoint, + condition: &Boolean, +) -> Result, SynthesisError> { + let c = AllocatedPoint { + x: conditionally_select( + cs.namespace(|| "x = cond ? a.x : b.x"), + &a.x, + &b.x, + condition, + )?, + y: conditionally_select( + cs.namespace(|| "y = cond ? a.y : b.y"), + &a.y, + &b.y, + condition, + )?, + is_infinity: conditionally_select( + cs.namespace(|| "is_infinity = cond ? a.is_infinity : b.is_infinity"), + &a.is_infinity, + &b.is_infinity, + condition, + )?, + }; + Ok(c) } diff --git a/src/gadgets/utils.rs b/src/gadgets/utils.rs index 9a9ca5a9..1375e39d 100644 --- a/src/gadgets/utils.rs +++ b/src/gadgets/utils.rs @@ -72,6 +72,26 @@ pub fn alloc_one>( Ok(one) } +#[allow(dead_code)] +/// alloc a field as a constant +/// implemented refer from https://github.com/lurk-lab/lurk-rs/blob/4335fbb3290ed1a1176e29428f7daacb47f8033d/src/circuit/gadgets/data.rs#L387-L402 +pub fn alloc_const>( + mut cs: CS, + val: F, +) -> Result, SynthesisError> { + let allocated = AllocatedNum::::alloc(cs.namespace(|| "allocate const"), || Ok(val))?; + + // allocated * 1 = val + cs.enforce( + || "enforce constant", + |lc| lc + allocated.get_variable(), + |lc| lc + CS::one(), + |_| Boolean::Constant(true).lc(CS::one(), val), + ); + + Ok(allocated) +} + /// Allocate a scalar as a base. Only to be used is the scalar fits in base! pub fn alloc_scalar_as_base( mut cs: CS, @@ -430,3 +450,22 @@ pub fn select_num_or_one>( Ok(c) } + +#[allow(dead_code)] +/// c = a + b where a, b is AllocatedNum +pub fn add_allocated_num>( + mut cs: CS, + a: &AllocatedNum, + b: &AllocatedNum, +) -> Result, SynthesisError> { + let c = AllocatedNum::alloc(cs.namespace(|| "c"), || { + Ok(*a.get_value().get()? + b.get_value().get()?) + })?; + cs.enforce( + || "Check u_fold", + |lc| lc + a.get_variable() + b.get_variable(), + |lc| lc + CS::one(), + |lc| lc + c.get_variable(), + ); + Ok(c) +} diff --git a/src/lib.rs b/src/lib.rs index 5aeb3663..52affba8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ pub mod provider; pub mod spartan; pub mod traits; +#[cfg(feature = "supernova")] +pub mod supernova; + use crate::bellpepper::{ r1cs::{NovaShape, NovaWitness}, shape_cs::ShapeCS, @@ -108,7 +111,7 @@ where ); let mut cs: ShapeCS = ShapeCS::new(); let _ = circuit_primary.synthesize(&mut cs); - let (r1cs_shape_primary, ck_primary) = cs.r1cs_shape(); + let (r1cs_shape_primary, ck_primary) = cs.r1cs_shape_with_commitmentkey(); // Initialize ck for the secondary let circuit_secondary: NovaAugmentedCircuit<'_, G1, C2> = NovaAugmentedCircuit::new( @@ -119,7 +122,7 @@ where ); let mut cs: ShapeCS = ShapeCS::new(); let _ = circuit_secondary.synthesize(&mut cs); - let (r1cs_shape_secondary, ck_secondary) = cs.r1cs_shape(); + let (r1cs_shape_secondary, ck_secondary) = cs.r1cs_shape_with_commitmentkey(); let mut pp = Self { F_arity_primary, @@ -140,7 +143,7 @@ where }; // set the digest in pp - pp.digest = compute_digest::>(&pp); + pp.digest = compute_digest::>(&[&pp]); pp } @@ -312,7 +315,7 @@ where return Err(NovaError::InvalidInitialInputLength); } - // Frist step was already done in the constructor + // First step was already done in the constructor if self.i == 0 { self.i = 1; return Ok(()); @@ -788,9 +791,12 @@ type Commitment = <::CE as CommitmentEngineTrait>::Commitment; type CompressedCommitment = <<::CE as CommitmentEngineTrait>::Commitment as CommitmentTrait>::CompressedCommitment; type CE = ::CE; -fn compute_digest(o: &T) -> G::Scalar { +/// compute digest giving a collection of Serialize object +pub fn compute_digest(o: &[&T]) -> G::Scalar { // obtain a vector of bytes representing public parameters - let bytes = bincode::serialize(o).unwrap(); + let mut bytes = vec![]; + o.iter() + .for_each(|o| bytes.extend(bincode::serialize(*o).unwrap())); // convert pp_bytes into a short digest let mut hasher = Sha3_256::new(); hasher.update(&bytes); diff --git a/src/nifs.rs b/src/nifs.rs index 51b98094..3aa2a7af 100644 --- a/src/nifs.rs +++ b/src/nifs.rs @@ -174,7 +174,7 @@ mod tests { // First create the shape let mut cs: TestShapeCS = TestShapeCS::new(); let _ = synthesize_tiny_r1cs_bellpepper(&mut cs, None); - let (shape, ck) = cs.r1cs_shape(); + let (shape, ck) = cs.r1cs_shape_with_commitmentkey(); let ro_consts = <::RO as ROTrait<::Base, ::Scalar>>::Constants::new(); diff --git a/src/r1cs.rs b/src/r1cs.rs index 75fc8d66..a9af1955 100644 --- a/src/r1cs.rs +++ b/src/r1cs.rs @@ -192,19 +192,23 @@ impl R1CSShape { assert_eq!(U.X.len(), self.num_io); // verify if Az * Bz = u*Cz + E - let res_eq: bool = { + let res_eq: Result<(), NovaError> = { let z = [W.W.clone(), vec![U.u], U.X.clone()].concat(); let (Az, Bz, Cz) = self.multiply_vec(&z)?; assert_eq!(Az.len(), self.num_cons); assert_eq!(Bz.len(), self.num_cons); assert_eq!(Cz.len(), self.num_cons); - let res: usize = (0..self.num_cons) - .map(|i| usize::from(Az[i] * Bz[i] != U.u * Cz[i] + W.E[i])) - .sum(); - - res == 0 + (0..self.num_cons).try_for_each(|i| { + if Az[i] * Bz[i] != U.u * Cz[i] + W.E[i] { + // constraint failed + Err(NovaError::UnSatIndex(i)) + } else { + Ok(()) + } + }) }; + res_eq?; // verify if comm_E and comm_W are commitments to E and W let res_comm: bool = { @@ -213,11 +217,10 @@ impl R1CSShape { U.comm_W == comm_W && U.comm_E == comm_E }; - if res_eq && res_comm { - Ok(()) - } else { - Err(NovaError::UnSat) + if !res_comm { + return Err(NovaError::UnSat); } + Ok(()) } /// Checks if the R1CS instance is satisfiable given a witness and its shape @@ -231,28 +234,30 @@ impl R1CSShape { assert_eq!(U.X.len(), self.num_io); // verify if Az * Bz = u*Cz - let res_eq: bool = { + let res_eq: Result<(), NovaError> = { let z = [W.W.clone(), vec![G::Scalar::ONE], U.X.clone()].concat(); let (Az, Bz, Cz) = self.multiply_vec(&z)?; assert_eq!(Az.len(), self.num_cons); assert_eq!(Bz.len(), self.num_cons); assert_eq!(Cz.len(), self.num_cons); - let res: usize = (0..self.num_cons) - .map(|i| usize::from(Az[i] * Bz[i] != Cz[i])) - .sum(); - - res == 0 + (0..self.num_cons).try_for_each(|i| { + if Az[i] * Bz[i] != Cz[i] { + // constraint failed, retrieve constaint name + Err(NovaError::UnSatIndex(i)) + } else { + Ok(()) + } + }) }; + res_eq?; // verify if comm_W is a commitment to W let res_comm: bool = U.comm_W == CE::::commit(ck, &W.W); - - if res_eq && res_comm { - Ok(()) - } else { - Err(NovaError::UnSat) + if !res_comm { + return Err(NovaError::UnSat); } + Ok(()) } /// A method to compute a commitment to the cross-term `T` given a diff --git a/src/spartan/direct.rs b/src/spartan/direct.rs index 5a7dd6c0..201fbd00 100644 --- a/src/spartan/direct.rs +++ b/src/spartan/direct.rs @@ -97,7 +97,7 @@ impl, C: StepCircuit> DirectSNA let mut cs: ShapeCS = ShapeCS::new(); let _ = circuit.synthesize(&mut cs); - let (shape, ck) = cs.r1cs_shape(); + let (shape, ck) = cs.r1cs_shape_with_commitmentkey(); let (pk, vk) = S::setup(&ck, &shape)?; diff --git a/src/spartan/ppsnark.rs b/src/spartan/ppsnark.rs index 1fb913fe..6f2659e4 100644 --- a/src/spartan/ppsnark.rs +++ b/src/spartan/ppsnark.rs @@ -853,7 +853,7 @@ impl> RelaxedR1CSSNARKTrait for Relaxe vk_ee, digest: G::Scalar::ZERO, }; - vk.digest = compute_digest::>(&vk); + vk.digest = compute_digest::>(&[&vk]); vk }; diff --git a/src/spartan/snark.rs b/src/spartan/snark.rs index 4827f568..24ab0a7a 100644 --- a/src/spartan/snark.rs +++ b/src/spartan/snark.rs @@ -76,7 +76,7 @@ impl> RelaxedR1CSSNARKTrait for Relaxe S: S.clone(), digest: G::Scalar::ZERO, }; - vk.digest = compute_digest::>(&vk); + vk.digest = compute_digest::>(&[&vk]); vk }; diff --git a/src/supernova/circuit.rs b/src/supernova/circuit.rs new file mode 100644 index 00000000..53424551 --- /dev/null +++ b/src/supernova/circuit.rs @@ -0,0 +1,603 @@ +//! Supernova implemetation support arbitrary argumented circuits and running instances. +//! There are two Verification Circuits for each argumented circuit: 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 circuit takes as input 2 hashes. +//! Each circuit folds the last invocation of the other into the respective running instance, specified by augmented_circuit_index +//! +//! The augmented circuit F' for SuperNova that includes everything from Nova +//! and additionally checks: +//! 1. Ui[] are contained in X[0] hash pre-image. +//! 2. R1CS Instance u is folded into Ui[augmented_circuit_index] correctly; just like Nova IVC. +//! 3. (optional by F logic) F circuit might check program_counter_{i} invoked current F circuit is legal or not. +//! 3. F circuit produce program_counter_{i+1} and sent to next round for optionally constraint the next F' argumented circuit. + +use crate::{ + constants::NUM_HASH_BITS, + gadgets::{ + ecc::AllocatedPoint, + r1cs::{ + conditionally_select_alloc_relaxed_r1cs, + conditionally_select_vec_allocated_relaxed_r1cs_instance, AllocatedR1CSInstance, + AllocatedRelaxedR1CSInstance, + }, + utils::{ + alloc_const, alloc_num_equals, alloc_scalar_as_base, alloc_zero, conditionally_select_vec, + le_bits_to_num, scalar_as_base, + }, + }, + r1cs::{R1CSInstance, RelaxedR1CSInstance}, + traits::{ + circuit_supernova::StepCircuit, commitment::CommitmentTrait, Group, ROCircuitTrait, + ROConstantsCircuit, + }, + Commitment, +}; +use bellpepper_core::{ + boolean::{AllocatedBit, Boolean}, + num::AllocatedNum, + ConstraintSystem, SynthesisError, +}; + +use bellpepper::gadgets::Assignment; + +use ff::Field; +use serde::{Deserialize, Serialize}; + +use super::utils::get_from_vec_alloc_relaxed_r1cs; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SuperNovaAugmentedCircuitParams { + limb_width: usize, + n_limbs: usize, + is_primary_circuit: bool, // A boolean indicating if this is the primary circuit +} + +impl SuperNovaAugmentedCircuitParams { + pub const fn new(limb_width: usize, n_limbs: usize, is_primary_circuit: bool) -> Self { + Self { + limb_width, + n_limbs, + is_primary_circuit, + } + } + + pub fn get_n_limbs(&self) -> usize { + self.n_limbs + } +} + +#[derive(Debug)] +pub struct SuperNovaAugmentedCircuitInputs<'a, G: Group> { + pp_digest: G::Scalar, + i: G::Base, + z0: &'a [G::Base], + zi: Option<&'a [G::Base]>, + U: Option<&'a [Option>]>, + u: Option<&'a R1CSInstance>, + T: Option<&'a Commitment>, + program_counter: G::Base, + last_augmented_circuit_index: G::Base, +} + +impl<'a, G: Group> SuperNovaAugmentedCircuitInputs<'a, G> { + /// Create new inputs/witness for the verification circuit + #[allow(clippy::too_many_arguments)] + pub fn new( + pp_digest: G::Scalar, + i: G::Base, + z0: &'a [G::Base], + zi: Option<&'a [G::Base]>, + U: Option<&'a [Option>]>, + u: Option<&'a R1CSInstance>, + T: Option<&'a Commitment>, + program_counter: G::Base, + last_augmented_circuit_index: G::Base, + ) -> Self { + Self { + pp_digest, + i, + z0, + zi, + U, + u, + T, + program_counter, + last_augmented_circuit_index, + } + } +} + +/// The augmented circuit F' in SuperNova that includes a step circuit F +/// and the circuit for the verifier in SuperNova's non-interactive folding scheme, +/// SuperNova NIFS will fold strictly r1cs instance u with respective relaxed r1cs instance U[last_augmented_circuit_index] +pub struct SuperNovaAugmentedCircuit<'a, G: Group, SC: StepCircuit> { + params: &'a SuperNovaAugmentedCircuitParams, + ro_consts: ROConstantsCircuit, + inputs: Option>, + step_circuit: &'a SC, // The function that is applied for each step + num_augmented_circuits: usize, // number of overall augmented circuits +} + +impl<'a, G: Group, SC: StepCircuit> SuperNovaAugmentedCircuit<'a, G, SC> { + /// Create a new verification circuit for the input relaxed r1cs instances + pub const fn new( + params: &'a SuperNovaAugmentedCircuitParams, + inputs: Option>, + step_circuit: &'a SC, + ro_consts: ROConstantsCircuit, + num_augmented_circuits: usize, + ) -> Self { + Self { + params, + inputs, + step_circuit, + ro_consts, + num_augmented_circuits, + } + } + + /// Allocate all witnesses and return + fn alloc_witness::Base>>( + &self, + mut cs: CS, + arity: usize, + num_augmented_circuits: usize, + ) -> Result< + ( + AllocatedNum, + AllocatedNum, + Vec>, + Vec>, + Vec>, + AllocatedR1CSInstance, + AllocatedPoint, + Option>, + AllocatedNum, + ), + SynthesisError, + > { + let last_augmented_circuit_index = + AllocatedNum::alloc(cs.namespace(|| "last_augmented_circuit_index"), || { + Ok(self.inputs.get()?.last_augmented_circuit_index) + })?; + + // Allocate the params + let params = alloc_scalar_as_base::( + cs.namespace(|| "params"), + self + .inputs + .get() + .map_or(None, |inputs| Some(inputs.pp_digest)), + )?; + + // Allocate i + let i = AllocatedNum::alloc(cs.namespace(|| "i"), || Ok(self.inputs.get()?.i))?; + + // Allocate program_counter only on primary circuit + let program_counter = if self.params.is_primary_circuit { + Some(AllocatedNum::alloc( + cs.namespace(|| "program_counter"), + || Ok(self.inputs.get()?.program_counter), + )?) + } else { + None + }; + + // Allocate z0 + let z_0 = (0..arity) + .map(|i| { + AllocatedNum::alloc(cs.namespace(|| format!("z0_{i}")), || { + Ok(self.inputs.get()?.z0[i]) + }) + }) + .collect::>, _>>()?; + + // Allocate zi. If inputs.zi is not provided (base case) allocate default value 0 + let zero = vec![G::Base::ZERO; arity]; + let z_i = (0..arity) + .map(|i| { + AllocatedNum::alloc(cs.namespace(|| format!("zi_{i}")), || { + Ok(self.inputs.get()?.zi.unwrap_or(&zero)[i]) + }) + }) + .collect::>, _>>()?; + + // Allocate the running instances + let U = (0..num_augmented_circuits) + .map(|i| { + AllocatedRelaxedR1CSInstance::alloc( + cs.namespace(|| format!("Allocate U {:?}", i)), + self + .inputs + .get() + .map_or(None, |inputs| inputs.U.and_then(|U| U[i].as_ref())), + self.params.limb_width, + self.params.n_limbs, + ) + }) + .collect::>, _>>()?; + + // Allocate the r1cs instance 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().map_or(None, |u| Some(u))), + )?; + + // Allocate T + let T = AllocatedPoint::alloc( + cs.namespace(|| "allocate T"), + self.inputs.get().map_or(None, |inputs| { + inputs.T.get().map_or(None, |T| Some(T.to_coordinates())) + }), + )?; + + Ok(( + params, + i, + z_0, + z_i, + U, + u, + T, + program_counter, + last_augmented_circuit_index, + )) + } + + /// Synthesizes base case and returns the new relaxed R1CSInstance + fn synthesize_base_case::Base>>( + &self, + mut cs: CS, + u: AllocatedR1CSInstance, + last_augmented_circuit_index_checked: &AllocatedNum, + num_augmented_circuits: usize, + ) -> Result>, SynthesisError> { + let mut cs = cs.namespace(|| "alloc U_i default"); + + // The primary circuit just initialize single AllocatedRelaxedR1CSInstance + let U_default = if self.params.is_primary_circuit { + vec![AllocatedRelaxedR1CSInstance::default( + cs.namespace(|| "Allocate primary U_default".to_string()), + self.params.limb_width, + self.params.n_limbs, + )?] + } else { + // The secondary circuit convert the incoming R1CS instance on index which match last_augmented_circuit_index + let incoming_r1cs = AllocatedRelaxedR1CSInstance::from_r1cs_instance( + cs.namespace(|| "Allocate incoming_r1cs"), + u, + self.params.limb_width, + self.params.n_limbs, + )?; + (0..num_augmented_circuits) + .map(|i| { + let i_alloc = alloc_const( + cs.namespace(|| format!("i allocated on {:?}", i)), + scalar_as_base::(G::Scalar::from(i as u64)), + )?; + let equal_bit = Boolean::from(alloc_num_equals( + cs.namespace(|| format!("check equal bit {:?}", i)), + &i_alloc, + last_augmented_circuit_index_checked, + )?); + let default = &AllocatedRelaxedR1CSInstance::default( + cs.namespace(|| format!("Allocate U_default {:?}", i)), + self.params.limb_width, + self.params.n_limbs, + )?; + conditionally_select_alloc_relaxed_r1cs( + cs.namespace(|| format!("select on index namespace {:?}", i)), + &incoming_r1cs, + default, + &equal_bit, + ) + }) + .collect::>, _>>()? + }; + 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: AllocatedNum, + z_0: Vec>, + z_i: Vec>, + U: &[AllocatedRelaxedR1CSInstance], + u: AllocatedR1CSInstance, + T: AllocatedPoint, + arity: usize, + last_augmented_circuit_index: &AllocatedNum, + program_counter: Option>, + num_augmented_circuits: usize, + ) -> Result< + ( + AllocatedNum, + Vec>, + AllocatedBit, + ), + SynthesisError, + > { + // Check that u.x[0] = Hash(params, i, program_counter, U[], z0, zi) + let mut ro = G::ROCircuit::new( + self.ro_consts.clone(), + 2 // params_next, i_new + + program_counter.as_ref().map_or(0, |_| 1) // optional program counter + + 2 * arity // zo, z1 + + num_augmented_circuits * (7 + 2 * self.params.n_limbs), // #num_augmented_circuits * (7 + [X0, X1]*#num_limb) + ); + ro.absorb(¶ms); + ro.absorb(&i); + if let Some(program_counter) = program_counter.as_ref() { + ro.absorb(program_counter) + } + + for e in &z_0 { + ro.absorb(e); + } + for e in &z_i { + ro.absorb(e); + } + + U.iter().enumerate().try_for_each(|(i, U)| { + U.absorb_in_ro(cs.namespace(|| format!("absorb U {:?}", i)), &mut ro) + })?; + + let hash_bits = ro.squeeze(cs.namespace(|| "Input hash"), NUM_HASH_BITS)?; + let hash = le_bits_to_num(cs.namespace(|| "bits to hash"), &hash_bits)?; + let check_pass: AllocatedBit = alloc_num_equals( + cs.namespace(|| "check consistency of u.X[0] with H(params, U, i, z0, zi)"), + &u.X0, + &hash, + )?; + + // Run NIFS Verifier + let (last_augmented_circuit_index_checked, U_to_fold) = get_from_vec_alloc_relaxed_r1cs( + cs.namespace(|| "U to fold"), + U, + last_augmented_circuit_index, + )?; + let U_fold = U_to_fold.fold_with_r1cs( + cs.namespace(|| "compute fold of U and u"), + ¶ms, + &u, + &T, + self.ro_consts.clone(), + self.params.limb_width, + self.params.n_limbs, + )?; + + // update AllocatedRelaxedR1CSInstance on index match augmented circuit index + let U_next: Vec> = U + .iter() + .enumerate() + .map(|(i, U)| { + let mut cs = cs.namespace(|| format!("U_i+1 non_base conditional selection {:?}", i)); + let i_alloc = alloc_const( + cs.namespace(|| "i allocated"), + scalar_as_base::(G::Scalar::from(i as u64)), + )?; + let equal_bit = Boolean::from(alloc_num_equals( + cs.namespace(|| "check equal bit"), + &i_alloc, + &last_augmented_circuit_index_checked, + )?); + conditionally_select_alloc_relaxed_r1cs( + cs.namespace(|| "select on index namespace"), + &U_fold, + U, + &equal_bit, + ) + }) + .collect::>, _>>()?; + + Ok((last_augmented_circuit_index_checked, U_next, check_pass)) + } +} + +impl<'a, G: Group, SC: StepCircuit> SuperNovaAugmentedCircuit<'a, G, SC> { + pub fn synthesize::Base>>( + self, + cs: &mut CS, + ) -> Result<(AllocatedNum, Vec>), SynthesisError> { + // NOTE `last_augmented_circuit_index` is aux without any constraint. + // Reason is prover can only produce valid running instance by folding u into proper U_i[last_augmented_circuit_index] + // However, there is crucial pre-asumption: `last_augmented_circuit_index` must within range [0, num_augmented_circuits) + // otherwise there will be a soundness error, such that maliculous prover can choose out of range last_augmented_circuit_index. + // The soundness error depends on how we process out-of-range condition. + // + // there are 2 possible solution + // 1. range check `last_augmented_circuit_index` + // 2. if last_augmented_circuit_index out of range, then by default select index 0 + // + // For current version we choose 2, due to its simplicify and fit well in last_augmented_circuit_index use case. + // Recap, the only way to pass running instance check is folding u into respective U_i[last_augmented_circuit_index] + // So, a circuit implementing to set out-of-range last_augmented_circuit_index to index 0 is fine. + // The illegal running instances will be propogate to later phase and finally captured with "high" probability on the basis of Nova IVC security. + // + // Although above "informal" analysis implies there is no `malleability` on statement (malleability refer `NARK.8 Malleability of Nova’s IVC` https://eprint.iacr.org/2023/969.pdf ) + // We need to carefully check whether it lead to other vulnerability. + + let arity = self.step_circuit.arity(); + let num_augmented_circuits = if self.params.is_primary_circuit { + // primary circuit only fold single running instance with secondary output strict r1cs instance + 1 + } else { + // secondary circuit contains the logic to choose one of multiple augments running instance to fold + self.num_augmented_circuits + }; + + if self.inputs.is_some() { + let z0_len = self.inputs.get().map_or(0, |inputs| inputs.z0.len()); + if self.step_circuit.arity() != z0_len { + return Err(SynthesisError::IncompatibleLengthVector(format!( + "z0_len {:?} != arity lengh {:?}", + z0_len, + self.step_circuit.arity() + ))); + } + let last_augmented_circuit_index = self + .inputs + .get() + .map_or(G::Base::ZERO, |inputs| inputs.last_augmented_circuit_index); + if self.params.is_primary_circuit && last_augmented_circuit_index != G::Base::ZERO { + return Err(SynthesisError::IncompatibleLengthVector( + "primary circuit running instance only valid on index 0".to_string(), + )); + } + } + + // Allocate witnesses + let (params, i, z_0, z_i, U, u, T, program_counter, last_augmented_circuit_index) = self + .alloc_witness( + cs.namespace(|| "allocate the circuit witness"), + arity, + num_augmented_circuits, + )?; + + // Compute variable indicating if this is the base case + let zero = alloc_zero(cs.namespace(|| "zero"))?; + let is_base_case = alloc_num_equals(cs.namespace(|| "Check if base case"), &i.clone(), &zero)?; + + // Synthesize the circuit for the non-base case and get the new running + // instances along with a boolean indicating if all checks have passed + // must use return `last_augmented_circuit_index_checked` since it got range checked + let (last_augmented_circuit_index_checked, U_next_non_base, check_non_base_pass) = self + .synthesize_non_base_case( + cs.namespace(|| "synthesize non base case"), + params.clone(), + i.clone(), + z_0.clone(), + z_i.clone(), + &U, + u.clone(), + T, + arity, + &last_augmented_circuit_index, + program_counter.clone(), + num_augmented_circuits, + )?; + + // Synthesize the circuit for the base case and get the new running instances + let U_next_base = self.synthesize_base_case( + cs.namespace(|| "base case"), + u.clone(), + &last_augmented_circuit_index_checked, + num_augmented_circuits, + )?; + + // 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_next + let U_next = conditionally_select_vec_allocated_relaxed_r1cs_instance( + cs.namespace(|| "U_next"), + &U_next_base[..], + &U_next_non_base[..], + &Boolean::from(is_base_case.clone()), + )?; + + // Compute i + 1 + let i_next = AllocatedNum::alloc(cs.namespace(|| "i + 1"), || { + Ok(*i.get_value().get()? + G::Base::ONE) + })?; + cs.enforce( + || "check i + 1", + |lc| lc + i.get_variable() + CS::one(), + |lc| lc + CS::one(), + |lc| lc + i_next.get_variable(), + ); + + // Compute z_{i+1} + let z_input = conditionally_select_vec( + cs.namespace(|| "select input to F"), + &z_0, + &z_i, + &Boolean::from(is_base_case), + )?; + + let (program_counter_new, z_next) = if let Some(program_counter) = &program_counter { + self + .step_circuit + .synthesize(&mut cs.namespace(|| "F"), program_counter, &z_input)? + } else { + let zero_program_counter = alloc_zero(cs.namespace(|| "zero pc"))?; + self + .step_circuit + .synthesize(&mut cs.namespace(|| "F"), &zero_program_counter, &z_input)? + }; + if z_next.len() != arity { + return Err(SynthesisError::IncompatibleLengthVector( + "z_next".to_string(), + )); + } + + // To check correct folding sequencing we are just going to make a hash. + // The next RunningInstance folding can take the pre-image of this hash as witness and check. + + // "Finally, there is a subtle sizing issue in the above description: in each step, + // because Ui+1 is produced as the public IO of F0 program_counter+1, it must be contained in + // the public IO of instance ui+1. In the next iteration, because ui+1 is folded + // into Ui+1[program_counter+1], this means that Ui+1[program_counter+1] is at least as large as Ui by the + // properties of the folding scheme. This means that the list of running instances + // grows in each step. To alleviate this issue, we have each F0j only produce a hash + // of its outputs as public output. In the subsequent step, the next augmented + // function takes as non-deterministic input a preimage to this hash." pg.16 + + // https://eprint.iacr.org/2022/1758.pdf + + // Compute the new hash H(params, i+1, program_counter, z0, z_{i+1}, U_next) + let mut ro = G::ROCircuit::new( + self.ro_consts.clone(), + 2 // params_next, i_new + + program_counter.as_ref().map_or(0, |_| 1) // optional program counter + + 2 * arity // zo, z1 + + num_augmented_circuits * (7 + 2 * self.params.n_limbs), // #num_augmented_circuits * (7 + [X0, X1]*#num_limb) + ); + ro.absorb(¶ms); + ro.absorb(&i_next); + // optionally absorb program counter if exist + if program_counter.is_some() { + ro.absorb(&program_counter_new) + } + for e in &z_0 { + ro.absorb(e); + } + for e in &z_next { + ro.absorb(e); + } + U_next.iter().enumerate().try_for_each(|(i, U)| { + U.absorb_in_ro(cs.namespace(|| format!("absorb U_new {:?}", i)), &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)?; + + // We are cycling of curve implementation, so primary/secondary will rotate hash in IO for the others to check + // bypass unmodified hash of other circuit as next X[0] + // and output the computed the computed hash as next X[1] + u.X1 + .inputize(cs.namespace(|| "bypass unmodified hash of the other circuit"))?; + hash.inputize(cs.namespace(|| "output new hash of this circuit"))?; + + Ok((program_counter_new, z_next)) + } +} diff --git a/src/supernova/error.rs b/src/supernova/error.rs new file mode 100644 index 00000000..797eb720 --- /dev/null +++ b/src/supernova/error.rs @@ -0,0 +1,19 @@ +//! This module defines errors returned by the library. +use core::fmt::Debug; +use thiserror::Error; + +use crate::errors::NovaError; + +/// Errors returned by Nova +#[derive(Clone, Debug, Eq, PartialEq, Error)] +pub enum SuperNovaError { + /// Nova error + #[error("NovaError")] + NovaError(NovaError), + /// missig commitment key + #[error("MissingCK")] + MissingCK, + /// Extended error for supernova + #[error("UnSatIndex")] + UnSatIndex(&'static str, usize), +} diff --git a/src/supernova/mod.rs b/src/supernova/mod.rs new file mode 100644 index 00000000..e8f15710 --- /dev/null +++ b/src/supernova/mod.rs @@ -0,0 +1,781 @@ +//! This library implements SuperNova, a Non-Uniform IVC based on Nova. + +use std::marker::PhantomData; + +use crate::{ + bellpepper::shape_cs::ShapeCS, + constants::{BN_LIMB_WIDTH, BN_N_LIMBS, NUM_HASH_BITS}, + errors::NovaError, + r1cs::{R1CSInstance, R1CSShape, R1CSWitness, RelaxedR1CSInstance, RelaxedR1CSWitness, R1CS}, + scalar_as_base, + traits::{ + circuit_supernova::StepCircuit, commitment::CommitmentTrait, AbsorbInROTrait, Group, + ROConstants, ROConstantsCircuit, ROConstantsTrait, ROTrait, + }, + Commitment, CommitmentKey, +}; + +use ff::Field; +use log::debug; +use serde::{Deserialize, Serialize}; + +use crate::bellpepper::{ + r1cs::{NovaShape, NovaWitness}, + solver::SatisfyingAssignment, +}; +use bellpepper_core::ConstraintSystem; + +use crate::nifs::NIFS; + +mod circuit; // declare the module first +use circuit::{ + SuperNovaAugmentedCircuit, SuperNovaAugmentedCircuitInputs, SuperNovaAugmentedCircuitParams, +}; + +use self::error::SuperNovaError; + +pub mod error; +pub(crate) mod utils; + +#[cfg(test)] +mod test; + +/// A type that holds public parameters of Nova +#[derive(Serialize, Deserialize)] +#[serde(bound = "")] +pub struct PublicParams +where + G1: Group::Scalar>, + G2: Group::Scalar>, +{ + F_arity_primary: usize, + F_arity_secondary: usize, + ro_consts_primary: ROConstants, + ro_consts_circuit_primary: ROConstantsCircuit, + ck_primary: Option>, + r1cs_shape_primary: R1CSShape, + ro_consts_secondary: ROConstants, + ro_consts_circuit_secondary: ROConstantsCircuit, + ck_secondary: Option>, + r1cs_shape_secondary: R1CSShape, + augmented_circuit_params_primary: SuperNovaAugmentedCircuitParams, + augmented_circuit_params_secondary: SuperNovaAugmentedCircuitParams, +} + +impl PublicParams +where + G1: Group::Scalar>, + G2: Group::Scalar>, +{ + /// Create a new `PublicParams` + pub fn setup_without_commitkey, C2: StepCircuit>( + c_primary: &C1, + c_secondary: &C2, + num_augmented_circuits: usize, + ) -> Self where { + let augmented_circuit_params_primary = + SuperNovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, true); + let augmented_circuit_params_secondary = + SuperNovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, false); + + let ro_consts_primary: ROConstants = ROConstants::::new(); + let ro_consts_secondary: ROConstants = ROConstants::::new(); + + 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::::new(); + let ro_consts_circuit_secondary: ROConstantsCircuit = ROConstantsCircuit::::new(); + + // Initialize ck for the primary + let circuit_primary: SuperNovaAugmentedCircuit<'_, G2, C1> = SuperNovaAugmentedCircuit::new( + &augmented_circuit_params_primary, + None, + c_primary, + ro_consts_circuit_primary.clone(), + num_augmented_circuits, + ); + let mut cs: ShapeCS = ShapeCS::new(); + let _ = circuit_primary.synthesize(&mut cs); + // We use the largest commitment_key for all instances + let r1cs_shape_primary = cs.r1cs_shape(); + + // Initialize ck for the secondary + let circuit_secondary: SuperNovaAugmentedCircuit<'_, G1, C2> = SuperNovaAugmentedCircuit::new( + &augmented_circuit_params_secondary, + None, + c_secondary, + ro_consts_circuit_secondary.clone(), + num_augmented_circuits, + ); + let mut cs: ShapeCS = ShapeCS::new(); + let _ = circuit_secondary.synthesize(&mut cs); + let r1cs_shape_secondary = cs.r1cs_shape(); + + Self { + F_arity_primary, + F_arity_secondary, + ro_consts_primary, + ro_consts_circuit_primary, + ck_primary: None, + r1cs_shape_primary, + ro_consts_secondary, + ro_consts_circuit_secondary, + ck_secondary: None, + r1cs_shape_secondary, + augmented_circuit_params_primary, + augmented_circuit_params_secondary, + } + } + + #[allow(dead_code)] + /// 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, + ) + } + + #[allow(dead_code)] + /// 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, + ) + } +} + +/// SuperNova takes Ui a list of running instances. +/// One instance of Ui is a struct called RunningClaim. +pub struct RunningClaim +where + G1: Group::Scalar>, + G2: Group::Scalar>, + Ca: StepCircuit, + Cb: StepCircuit, +{ + _phantom: PhantomData, + augmented_circuit_index: usize, + c_primary: Ca, + c_secondary: Cb, + params: PublicParams, +} + +impl RunningClaim +where + G1: Group::Scalar>, + G2: Group::Scalar>, + Ca: StepCircuit, + Cb: StepCircuit, +{ + /// new a running claim + pub fn new( + augmented_circuit_index: usize, + circuit_primary: Ca, + circuit_secondary: Cb, + num_augmented_circuits: usize, + ) -> Self { + let claim = circuit_primary; + + let pp = PublicParams::::setup_without_commitkey( + &claim, + &circuit_secondary, + num_augmented_circuits, + ); + + Self { + augmented_circuit_index, + _phantom: PhantomData, + c_primary: claim, + c_secondary: circuit_secondary, + params: pp, + } + } + + /// get augmented_circuit_index + pub fn get_augmented_circuit_index(&self) -> usize { + self.augmented_circuit_index + } + + /// set primary/secondary commitment key + pub fn set_commitmentkey( + &mut self, + ck_primary: CommitmentKey, + ck_secondary: CommitmentKey, + ) { + self.params.ck_primary = Some(ck_primary); + self.params.ck_secondary = Some(ck_secondary); + } + + /// get primary/secondary circuit r1cs shape + pub fn get_r1cs_shape(&self) -> (&R1CSShape, &R1CSShape) { + ( + &self.params.r1cs_shape_primary, + &self.params.r1cs_shape_secondary, + ) + } + + /// get augmented_circuit_index + pub fn get_publicparams(&self) -> &PublicParams { + &self.params + } +} + +/// A SNARK that proves the correct execution of an non-uniform incremental computation +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct RecursiveSNARK +where + G1: Group::Scalar>, + G2: Group::Scalar>, +{ + r_W_primary: Vec>>, + r_U_primary: Vec>>, + r_W_secondary: Vec>>, // usually r_W_secondary.len() == 1 + r_U_secondary: Vec>>, // usually r_U_secondary.len() == 1 + l_w_secondary: R1CSWitness, + l_u_secondary: R1CSInstance, + pp_digest: G1::Scalar, + i: usize, + zi_primary: Vec, + zi_secondary: Vec, + program_counter: G1::Scalar, + augmented_circuit_index: usize, + num_augmented_circuits: usize, +} + +impl RecursiveSNARK +where + G1: Group::Scalar>, + G2: Group::Scalar>, +{ + /// iterate base step to get new instance of recursive SNARK + pub fn iter_base_step, C2: StepCircuit>( + claim: &RunningClaim, + pp_digest: G1::Scalar, + initial_program_counter: G1::Scalar, + first_augmented_circuit_index: usize, + num_augmented_circuits: usize, + z0_primary: &[G1::Scalar], + z0_secondary: &[G2::Scalar], + ) -> Result { + let pp = &claim.params; + let c_primary = &claim.c_primary; + let c_secondary = &claim.c_secondary; + // commitment key for primary & secondary circuit + let ck_primary = pp.ck_primary.as_ref().ok_or(SuperNovaError::MissingCK)?; + let ck_secondary = pp.ck_secondary.as_ref().ok_or(SuperNovaError::MissingCK)?; + + if z0_primary.len() != pp.F_arity_primary || z0_secondary.len() != pp.F_arity_secondary { + return Err(SuperNovaError::NovaError( + NovaError::InvalidStepOutputLength, + )); + } + + // base case for the primary + let mut cs_primary: SatisfyingAssignment = SatisfyingAssignment::new(); + let inputs_primary: SuperNovaAugmentedCircuitInputs<'_, G2> = + SuperNovaAugmentedCircuitInputs::new( + scalar_as_base::(pp_digest), + G1::Scalar::ZERO, + z0_primary, + None, + None, + None, + None, + initial_program_counter, + G1::Scalar::ZERO, // set augmented circuit index selector to 0 in base case + ); + + let circuit_primary: SuperNovaAugmentedCircuit<'_, G2, C1> = SuperNovaAugmentedCircuit::new( + &pp.augmented_circuit_params_primary, + Some(inputs_primary), + c_primary, + pp.ro_consts_circuit_primary.clone(), + num_augmented_circuits, + ); + + let (zi_primary_pc_next, zi_primary) = + circuit_primary.synthesize(&mut cs_primary).map_err(|err| { + debug!("err {:?}", err); + SuperNovaError::NovaError(NovaError::SynthesisError) + })?; + if zi_primary.len() != pp.F_arity_primary { + return Err(SuperNovaError::NovaError( + NovaError::InvalidStepOutputLength, + )); + } + let (u_primary, w_primary) = cs_primary + .r1cs_instance_and_witness(&pp.r1cs_shape_primary, ck_primary) + .map_err(|err| { + debug!("err {:?}", err); + SuperNovaError::NovaError(NovaError::SynthesisError) + })?; + + // base case for the secondary + let mut cs_secondary: SatisfyingAssignment = SatisfyingAssignment::new(); + let inputs_secondary: SuperNovaAugmentedCircuitInputs<'_, G1> = + SuperNovaAugmentedCircuitInputs::new( + pp_digest, + G2::Scalar::ZERO, + z0_secondary, + None, + None, + Some(&u_primary), + None, + G2::Scalar::ZERO, // secondary circuit never constrain/bump program counter + G2::Scalar::from(claim.augmented_circuit_index as u64), + ); + let circuit_secondary: SuperNovaAugmentedCircuit<'_, G1, C2> = SuperNovaAugmentedCircuit::new( + &pp.augmented_circuit_params_secondary, + Some(inputs_secondary), + c_secondary, + pp.ro_consts_circuit_secondary.clone(), + num_augmented_circuits, + ); + let (_, zi_secondary) = circuit_secondary + .synthesize(&mut cs_secondary) + .map_err(|_| SuperNovaError::NovaError(NovaError::SynthesisError))?; + if zi_secondary.len() != pp.F_arity_secondary { + return Err(SuperNovaError::NovaError( + NovaError::InvalidStepOutputLength, + )); + } + let (u_secondary, w_secondary) = cs_secondary + .r1cs_instance_and_witness(&pp.r1cs_shape_secondary, ck_secondary) + .map_err(|_| SuperNovaError::NovaError(NovaError::UnSat))?; + + // IVC proof for the primary circuit + let l_w_primary = w_primary; + let l_u_primary = u_primary; + let r_W_primary = RelaxedR1CSWitness::from_r1cs_witness(&pp.r1cs_shape_primary, &l_w_primary); + + let r_U_primary = + RelaxedR1CSInstance::from_r1cs_instance(ck_primary, &pp.r1cs_shape_primary, &l_u_primary); + + // IVC proof of the secondary circuit + let l_w_secondary = w_secondary; + let l_u_secondary = u_secondary; + let r_W_secondary = vec![Some(RelaxedR1CSWitness::::default( + &pp.r1cs_shape_secondary, + ))]; + let r_U_secondary = vec![Some(RelaxedR1CSInstance::default( + ck_secondary, + &pp.r1cs_shape_secondary, + ))]; + + // Outputs of the two circuits and next program counter thus far. + let zi_primary = zi_primary + .iter() + .map(|v| { + v.get_value() + .ok_or(SuperNovaError::NovaError(NovaError::SynthesisError)) + }) + .collect::::Scalar>, SuperNovaError>>()?; + let zi_primary_pc_next = zi_primary_pc_next + .get_value() + .ok_or(SuperNovaError::NovaError(NovaError::SynthesisError))?; + let zi_secondary = zi_secondary + .iter() + .map(|v| { + v.get_value() + .ok_or(SuperNovaError::NovaError(NovaError::SynthesisError)) + }) + .collect::::Scalar>, SuperNovaError>>()?; + + // handle the base case by initialize U_next in next round + let r_W_primary_initial_list = (0..num_augmented_circuits) + .map(|i| (i == first_augmented_circuit_index).then(|| r_W_primary.clone())) + .collect::>>>(); + + let r_U_primary_initial_list = (0..num_augmented_circuits) + .map(|i| (i == first_augmented_circuit_index).then(|| r_U_primary.clone())) + .collect::>>>(); + + Ok(Self { + r_W_primary: r_W_primary_initial_list, + r_U_primary: r_U_primary_initial_list, + r_W_secondary, + r_U_secondary, + l_w_secondary, + l_u_secondary, + pp_digest, + i: 0_usize, // after base case, next iteration start from 1 + zi_primary, + zi_secondary, + program_counter: zi_primary_pc_next, + augmented_circuit_index: first_augmented_circuit_index, + num_augmented_circuits, + }) + } + /// executing a step of the incremental computation + pub fn prove_step, C2: StepCircuit>( + &mut self, + claim: &RunningClaim, + z0_primary: &[G1::Scalar], + z0_secondary: &[G2::Scalar], + ) -> Result<(), SuperNovaError> { + // First step was already done in the constructor + if self.i == 0 { + self.i = 1; + return Ok(()); + } + + if self.r_U_secondary.len() != 1 || self.r_W_secondary.len() != 1 { + return Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)); + } + + let pp = &claim.params; + let c_primary = &claim.c_primary; + let c_secondary = &claim.c_secondary; + // commitment key for primary & secondary circuit + let ck_primary = pp.ck_primary.as_ref().ok_or(SuperNovaError::MissingCK)?; + let ck_secondary = pp.ck_secondary.as_ref().ok_or(SuperNovaError::MissingCK)?; + + if z0_primary.len() != pp.F_arity_primary || z0_secondary.len() != pp.F_arity_secondary { + return Err(SuperNovaError::NovaError( + NovaError::InvalidInitialInputLength, + )); + } + + // fold the secondary circuit's instance + let (nifs_secondary, (r_U_secondary_folded, r_W_secondary_folded)) = NIFS::prove( + ck_secondary, + &pp.ro_consts_secondary, + &scalar_as_base::(self.pp_digest), + &pp.r1cs_shape_secondary, + self.r_U_secondary[0].as_ref().unwrap(), + self.r_W_secondary[0].as_ref().unwrap(), + &self.l_u_secondary, + &self.l_w_secondary, + ) + .map_err(SuperNovaError::NovaError)?; + + // clone and updated running instance on respective circuit_index + let r_U_secondary_next = r_U_secondary_folded; + let r_W_secondary_next = r_W_secondary_folded; + + let mut cs_primary: SatisfyingAssignment = SatisfyingAssignment::new(); + let T = + Commitment::::decompress(&nifs_secondary.comm_T).map_err(SuperNovaError::NovaError)?; + let inputs_primary: SuperNovaAugmentedCircuitInputs<'_, G2> = + SuperNovaAugmentedCircuitInputs::new( + scalar_as_base::(self.pp_digest), + G1::Scalar::from(self.i as u64), + z0_primary, + Some(&self.zi_primary), + Some(&self.r_U_secondary), + Some(&self.l_u_secondary), + Some(&T), + self.program_counter, + G1::Scalar::ZERO, + ); + + let circuit_primary: SuperNovaAugmentedCircuit<'_, G2, C1> = SuperNovaAugmentedCircuit::new( + &pp.augmented_circuit_params_primary, + Some(inputs_primary), + c_primary, + pp.ro_consts_circuit_primary.clone(), + self.num_augmented_circuits, + ); + + let (zi_primary_pc_next, zi_primary) = circuit_primary + .synthesize(&mut cs_primary) + .map_err(|_| SuperNovaError::NovaError(NovaError::SynthesisError))?; + if zi_primary.len() != pp.F_arity_primary { + return Err(SuperNovaError::NovaError( + NovaError::InvalidInitialInputLength, + )); + } + + let (l_u_primary, l_w_primary) = cs_primary + .r1cs_instance_and_witness(&pp.r1cs_shape_primary, ck_primary) + .map_err(|_| SuperNovaError::NovaError(NovaError::UnSat))?; + + // Split into `if let`/`else` statement + // to avoid `returns a value referencing data owned by closure` error on `&RelaxedR1CSInstance::default` and `RelaxedR1CSWitness::default` + let (nifs_primary, (r_U_primary_folded, r_W_primary_folded)) = match ( + self.r_U_primary.get(claim.get_augmented_circuit_index()), + self.r_W_primary.get(claim.get_augmented_circuit_index()), + ) { + (Some(Some(r_U_primary)), Some(Some(r_W_primary))) => NIFS::prove( + ck_primary, + &pp.ro_consts_primary, + &self.pp_digest, + &pp.r1cs_shape_primary, + r_U_primary, + r_W_primary, + &l_u_primary, + &l_w_primary, + ) + .map_err(SuperNovaError::NovaError)?, + _ => NIFS::prove( + ck_primary, + &pp.ro_consts_primary, + &self.pp_digest, + &pp.r1cs_shape_primary, + &RelaxedR1CSInstance::default(ck_primary, &pp.r1cs_shape_primary), + &RelaxedR1CSWitness::default(&pp.r1cs_shape_primary), + &l_u_primary, + &l_w_primary, + ) + .map_err(SuperNovaError::NovaError)?, + }; + + let mut cs_secondary: SatisfyingAssignment = SatisfyingAssignment::new(); + let binding = + Commitment::::decompress(&nifs_primary.comm_T).map_err(SuperNovaError::NovaError)?; + let inputs_secondary: SuperNovaAugmentedCircuitInputs<'_, G1> = + SuperNovaAugmentedCircuitInputs::new( + self.pp_digest, + G2::Scalar::from(self.i as u64), + z0_secondary, + Some(&self.zi_secondary), + Some(&self.r_U_primary), + Some(&l_u_primary), + Some(&binding), + G2::Scalar::ZERO, // secondary circuit never constrain/bump program counter + G2::Scalar::from(claim.get_augmented_circuit_index() as u64), + ); + + let circuit_secondary: SuperNovaAugmentedCircuit<'_, G1, C2> = SuperNovaAugmentedCircuit::new( + &pp.augmented_circuit_params_secondary, + Some(inputs_secondary), + c_secondary, + pp.ro_consts_circuit_secondary.clone(), + self.num_augmented_circuits, + ); + let (_, zi_secondary) = circuit_secondary + .synthesize(&mut cs_secondary) + .map_err(|_| SuperNovaError::NovaError(NovaError::SynthesisError))?; + if zi_secondary.len() != pp.F_arity_secondary { + return Err(SuperNovaError::NovaError( + NovaError::InvalidInitialInputLength, + )); + } + + let (l_u_secondary_next, l_w_secondary_next) = cs_secondary + .r1cs_instance_and_witness(&pp.r1cs_shape_secondary, ck_secondary) + .map_err(|_| SuperNovaError::NovaError(NovaError::UnSat))?; + + // update the running instances and witnesses + let zi_primary = zi_primary + .iter() + .map(|v| { + v.get_value() + .ok_or(SuperNovaError::NovaError(NovaError::SynthesisError)) + }) + .collect::::Scalar>, SuperNovaError>>()?; + let zi_primary_pc_next = zi_primary_pc_next + .get_value() + .ok_or(SuperNovaError::NovaError(NovaError::SynthesisError))?; + let zi_secondary = zi_secondary + .iter() + .map(|v| { + v.get_value() + .ok_or(SuperNovaError::NovaError(NovaError::SynthesisError)) + }) + .collect::::Scalar>, SuperNovaError>>()?; + + if zi_primary.len() != pp.F_arity_primary || zi_secondary.len() != pp.F_arity_secondary { + return Err(SuperNovaError::NovaError( + NovaError::InvalidStepOutputLength, + )); + } + + // clone and updated running instance on respective circuit_index + self.r_U_primary[claim.get_augmented_circuit_index()] = Some(r_U_primary_folded); + self.r_W_primary[claim.get_augmented_circuit_index()] = Some(r_W_primary_folded); + self.r_W_secondary = vec![Some(r_W_secondary_next)]; + self.r_U_secondary = vec![Some(r_U_secondary_next)]; + self.l_w_secondary = l_w_secondary_next; + self.l_u_secondary = l_u_secondary_next; + self.i += 1; + self.zi_primary = zi_primary; + self.zi_secondary = zi_secondary; + self.program_counter = zi_primary_pc_next; + self.augmented_circuit_index = claim.get_augmented_circuit_index(); + Ok(()) + } + + /// verify recursive snark + pub fn verify, C2: StepCircuit>( + &mut self, + claim: &RunningClaim, + z0_primary: &[G1::Scalar], + z0_secondary: &[G2::Scalar], + ) -> Result<(), SuperNovaError> { + // number of steps cannot be zero + if self.i == 0 { + debug!("must verify on valid RecursiveSNARK where i > 0"); + return Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)); + } + + // check the (relaxed) R1CS instances public outputs. + if self.l_u_secondary.X.len() != 2 { + return Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)); + } + + if self.r_U_secondary.len() != 1 || self.r_W_secondary.len() != 1 { + return Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)); + } + + let pp = &claim.params; + let ck_primary = pp.ck_primary.as_ref().ok_or(SuperNovaError::MissingCK)?; + + self.r_U_primary[claim.get_augmented_circuit_index()] + .as_ref() + .map_or(Ok(()), |U| { + if U.X.len() != 2 { + debug!("r_U_primary got instance length {:?} != {:?}", U.X.len(), 2); + Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)) + } else { + Ok(()) + } + })?; + + self.r_U_secondary[0].as_ref().map_or(Ok(()), |U| { + if U.X.len() != 2 { + debug!( + "r_U_secondary got instance length {:?} != {:?}", + U.X.len(), + 2 + ); + Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)) + } else { + Ok(()) + } + })?; + + let num_field_primary_ro = 3 // params_next, i_new, program_counter_new + + 2 * pp.F_arity_primary // zo, z1 + + (7 + 2 * pp.augmented_circuit_params_primary.get_n_limbs()); // # 1 * (7 + [X0, X1]*#num_limb) + + // secondary circuit + let num_field_secondary_ro = 2 // params_next, i_new + + 2 * pp.F_arity_primary // zo, z1 + + self.num_augmented_circuits * (7 + 2 * pp.augmented_circuit_params_primary.get_n_limbs()); // #num_augmented_circuits * (7 + [X0, X1]*#num_limb) + + let (hash_primary, hash_secondary) = { + let mut hasher = ::RO::new(pp.ro_consts_secondary.clone(), num_field_primary_ro); + hasher.absorb(self.pp_digest); + hasher.absorb(G1::Scalar::from(self.i as u64)); + hasher.absorb(self.program_counter); + + for e in z0_primary { + hasher.absorb(*e); + } + for e in &self.zi_primary { + hasher.absorb(*e); + } + self.r_U_secondary[0].as_ref().map_or( + Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)), + |U| { + U.absorb_in_ro(&mut hasher); + Ok(()) + }, + )?; + + let mut hasher2 = + ::RO::new(pp.ro_consts_primary.clone(), num_field_secondary_ro); + hasher2.absorb(scalar_as_base::(self.pp_digest)); + hasher2.absorb(G2::Scalar::from(self.i as u64)); + + for e in z0_secondary { + hasher2.absorb(*e); + } + for e in &self.zi_secondary { + hasher2.absorb(*e); + } + let default_value = RelaxedR1CSInstance::default(ck_primary, &pp.r1cs_shape_primary); + self.r_U_primary.iter().for_each(|U| { + U.as_ref() + .unwrap_or(&default_value) + .absorb_in_ro(&mut hasher2); + }); + + ( + hasher.squeeze(NUM_HASH_BITS), + hasher2.squeeze(NUM_HASH_BITS), + ) + }; + + if hash_primary != self.l_u_secondary.X[0] { + debug!( + "hash_primary {:?} not equal l_u_secondary.X[0] {:?}", + hash_primary, self.l_u_secondary.X[0] + ); + return Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)); + } + if hash_secondary != scalar_as_base::(self.l_u_secondary.X[1]) { + debug!( + "hash_secondary {:?} not equal l_u_secondary.X[1] {:?}", + hash_secondary, self.l_u_secondary.X[1] + ); + return Err(SuperNovaError::NovaError(NovaError::ProofVerifyError)); + } + + // check the satisfiability of the provided `circuit_index` instance + let default_instance = RelaxedR1CSInstance::default(ck_primary, &pp.r1cs_shape_primary); + let default_witness = RelaxedR1CSWitness::default(&pp.r1cs_shape_primary); + let (res_r_primary, (res_r_secondary, res_l_secondary)) = rayon::join( + || { + pp.r1cs_shape_primary.is_sat_relaxed( + pp.ck_primary.as_ref().unwrap(), + self.r_U_primary[claim.get_augmented_circuit_index()] + .as_ref() + .unwrap_or(&default_instance), + self.r_W_primary[claim.get_augmented_circuit_index()] + .as_ref() + .unwrap_or(&default_witness), + ) + }, + || { + rayon::join( + || { + pp.r1cs_shape_secondary.is_sat_relaxed( + pp.ck_secondary.as_ref().unwrap(), + self.r_U_secondary[0].as_ref().unwrap(), + self.r_W_secondary[0].as_ref().unwrap(), + ) + }, + || { + pp.r1cs_shape_secondary.is_sat( + pp.ck_secondary.as_ref().unwrap(), + &self.l_u_secondary, + &self.l_w_secondary, + ) + }, + ) + }, + ); + + res_r_primary.map_err(|err| match err { + NovaError::UnSatIndex(i) => SuperNovaError::UnSatIndex("r_primary", i), + e => SuperNovaError::NovaError(e), + })?; + res_r_secondary.map_err(|err| match err { + NovaError::UnSatIndex(i) => SuperNovaError::UnSatIndex("r_secondary", i), + e => SuperNovaError::NovaError(e), + })?; + res_l_secondary.map_err(|err| match err { + NovaError::UnSatIndex(i) => SuperNovaError::UnSatIndex("l_secondary", i), + e => SuperNovaError::NovaError(e), + })?; + + Ok(()) + } + + /// get program counter + pub fn get_program_counter(&self) -> G1::Scalar { + self.program_counter + } +} + +/// genenate commitmentkey by r1cs shape +pub fn gen_commitmentkey_by_r1cs(shape: &R1CSShape) -> CommitmentKey { + R1CS::::commitment_key(shape) +} diff --git a/src/supernova/test.rs b/src/supernova/test.rs new file mode 100644 index 00000000..2314b777 --- /dev/null +++ b/src/supernova/test.rs @@ -0,0 +1,574 @@ +use crate::bellpepper::test_shape_cs::TestShapeCS; +use crate::gadgets::utils::alloc_const; +use crate::gadgets::utils::alloc_num_equals; +use crate::gadgets::utils::conditionally_select; +use crate::provider::poseidon::PoseidonConstantsCircuit; +use crate::traits::circuit_supernova::TrivialTestCircuit; +use crate::{ + compute_digest, + gadgets::utils::{add_allocated_num, alloc_one, alloc_zero}, +}; +use bellpepper::gadgets::boolean::Boolean; +use bellpepper_core::num::AllocatedNum; +use bellpepper_core::{ConstraintSystem, LinearCombination, SynthesisError}; +use core::marker::PhantomData; +use ff::Field; +use ff::PrimeField; + +use super::*; + +fn constraint_augmented_circuit_index>( + mut cs: CS, + pc_counter: &AllocatedNum, + rom: &[AllocatedNum], + circuit_index: &AllocatedNum, +) -> Result<(), SynthesisError> { + // select target when index match or empty + let zero = alloc_zero(cs.namespace(|| "zero"))?; + let rom_values = rom + .iter() + .enumerate() + .map(|(i, rom_value)| { + let index_alloc = alloc_const( + cs.namespace(|| format!("rom_values {} index ", i)), + F::from(i as u64), + )?; + let equal_bit = Boolean::from(alloc_num_equals( + cs.namespace(|| format!("rom_values {} equal bit", i)), + &index_alloc, + pc_counter, + )?); + conditionally_select( + cs.namespace(|| format!("rom_values {} conditionally_select ", i)), + rom_value, + &zero, + &equal_bit, + ) + }) + .collect::>, SynthesisError>>()?; + + let sum_lc = rom_values + .iter() + .fold(LinearCombination::::zero(), |acc_lc, row_value| { + acc_lc + row_value.get_variable() + }); + + cs.enforce( + || "sum_lc == circuit_index", + |lc| lc + circuit_index.get_variable() - &sum_lc, + |lc| lc + CS::one(), + |lc| lc, + ); + + Ok(()) +} + +#[derive(Clone, Debug, Default)] +struct CubicCircuit { + _p: PhantomData, + circuit_index: usize, + rom_size: usize, +} + +impl CubicCircuit +where + F: PrimeField, +{ + fn new(circuit_index: usize, rom_size: usize) -> Self { + CubicCircuit { + circuit_index, + rom_size, + _p: PhantomData, + } + } +} + +impl StepCircuit for CubicCircuit +where + F: PrimeField, +{ + fn arity(&self) -> usize { + 1 + self.rom_size // value + rom[].len() + } + + fn synthesize>( + &self, + cs: &mut CS, + pc_counter: &AllocatedNum, + z: &[AllocatedNum], + ) -> Result<(AllocatedNum, Vec>), SynthesisError> { + // constrain rom[pc] equal to `self.circuit_index` + let circuit_index = alloc_const( + cs.namespace(|| "circuit_index"), + F::from(self.circuit_index as u64), + )?; + constraint_augmented_circuit_index( + cs.namespace(|| "CubicCircuit agumented circuit constraint"), + pc_counter, + &z[1..], + &circuit_index, + )?; + + let one = alloc_one(cs.namespace(|| "alloc one"))?; + let pc_next = add_allocated_num( + // pc = pc + 1 + cs.namespace(|| "pc = pc + 1".to_string()), + pc_counter, + &one, + )?; + + // 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(), + ); + + let mut z_next = vec![y]; + z_next.extend(z[1..].iter().cloned()); + Ok((pc_next, z_next)) + } +} + +#[derive(Clone, Debug, Default)] +struct SquareCircuit { + _p: PhantomData, + circuit_index: usize, + rom_size: usize, +} + +impl SquareCircuit +where + F: PrimeField, +{ + fn new(circuit_index: usize, rom_size: usize) -> Self { + SquareCircuit { + circuit_index, + rom_size, + _p: PhantomData, + } + } +} + +impl StepCircuit for SquareCircuit +where + F: PrimeField, +{ + fn arity(&self) -> usize { + 1 + self.rom_size // value + rom[].len() + } + + fn synthesize>( + &self, + cs: &mut CS, + pc_counter: &AllocatedNum, + z: &[AllocatedNum], + ) -> Result<(AllocatedNum, Vec>), SynthesisError> { + // constrain rom[pc] equal to `self.circuit_index` + let circuit_index = alloc_const( + cs.namespace(|| "circuit_index"), + F::from(self.circuit_index as u64), + )?; + constraint_augmented_circuit_index( + cs.namespace(|| "SquareCircuit agumented circuit constraint"), + pc_counter, + &z[1..], + &circuit_index, + )?; + let one = alloc_one(cs.namespace(|| "alloc one"))?; + let pc_next = add_allocated_num( + // pc = pc + 1 + cs.namespace(|| "pc = pc + 1"), + pc_counter, + &one, + )?; + + // Consider an equation: `x^2 + 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 y = AllocatedNum::alloc(cs.namespace(|| "y"), || { + Ok(x_sq.get_value().unwrap() + x.get_value().unwrap() + F::from(5u64)) + })?; + + cs.enforce( + || "y = x^2 + x + 5", + |lc| { + lc + x_sq.get_variable() + + x.get_variable() + + CS::one() + + CS::one() + + CS::one() + + CS::one() + + CS::one() + }, + |lc| lc + CS::one(), + |lc| lc + y.get_variable(), + ); + + let mut z_next = vec![y]; + z_next.extend(z[1..].iter().cloned()); + Ok((pc_next, z_next)) + } +} + +fn print_constraints_name_on_error_index( + err: SuperNovaError, + running_claim: &RunningClaim, + num_augmented_circuits: usize, +) where + G1: Group::Scalar>, + G2: Group::Scalar>, + Ca: StepCircuit, + Cb: StepCircuit, +{ + match err { + SuperNovaError::UnSatIndex(msg, index) if msg == "r_primary" => { + let circuit_primary: SuperNovaAugmentedCircuit<'_, G2, Ca> = SuperNovaAugmentedCircuit::new( + &running_claim.params.augmented_circuit_params_primary, + None, + &running_claim.c_primary, + running_claim.params.ro_consts_circuit_primary.clone(), + num_augmented_circuits, + ); + let mut cs: TestShapeCS = TestShapeCS::new(); + let _ = circuit_primary.synthesize(&mut cs); + cs.constraints + .get(index) + .map(|constraint| debug!("{msg} failed at constraint {}", constraint.3)); + } + SuperNovaError::UnSatIndex(msg, index) if msg == "r_secondary" || msg == "l_secondary" => { + let circuit_secondary: SuperNovaAugmentedCircuit<'_, G1, Cb> = SuperNovaAugmentedCircuit::new( + &running_claim.params.augmented_circuit_params_secondary, + None, + &running_claim.c_secondary, + running_claim.params.ro_consts_circuit_secondary.clone(), + num_augmented_circuits, + ); + let mut cs: TestShapeCS = TestShapeCS::new(); + let _ = circuit_secondary.synthesize(&mut cs); + cs.constraints + .get(index) + .map(|constraint| debug!("{msg} failed at constraint {}", constraint.3)); + } + _ => (), + } +} + +const OPCODE_0: usize = 0; +const OPCODE_1: usize = 1; +fn test_trivial_nivc_with() +where + G1: Group::Scalar>, + G2: Group::Scalar>, +{ + // Here demo a simple RAM machine + // - with 2 argumented circuit + // - each argumented circuit contains primary and secondary circuit + // - a memory commmitment via a public IO `rom` (like a program) to constraint the sequence execution + + // This test also ready to add more argumented circuit and ROM can be arbitrary length + + // ROM is for constraints the sequence of execution order for opcode + // program counter initially point to 0 + + // TODO: replace with memory commitment along with suggestion from Supernova 4.4 optimisations + + // This is mostly done with the existing Nova code. With additions of U_i[] and program_counter checks + // in the augmented circuit. + + // To save the test time, after each step of iteration, RecursiveSNARK just verfiy the latest U_i[augmented_circuit_index] needs to be a satisfying instance. + // TODO At the end of this test, RecursiveSNARK need to verify all U_i[] are satisfying instances + + let rom = [ + OPCODE_1, OPCODE_1, OPCODE_0, OPCODE_0, OPCODE_1, OPCODE_1, OPCODE_0, OPCODE_0, OPCODE_1, + OPCODE_1, + ]; // Rom can be arbitrary length. + let circuit_secondary = TrivialTestCircuit::new(rom.len()); + let num_augmented_circuit = 2; + + // Structuring running claims + let test_circuit1 = CubicCircuit::new(OPCODE_0, rom.len()); + let mut running_claim1 = RunningClaim::< + G1, + G2, + CubicCircuit<::Scalar>, + TrivialTestCircuit<::Scalar>, + >::new( + OPCODE_0, + test_circuit1, + circuit_secondary.clone(), + num_augmented_circuit, + ); + + let test_circuit2 = SquareCircuit::new(OPCODE_1, rom.len()); + let mut running_claim2 = RunningClaim::< + G1, + G2, + SquareCircuit<::Scalar>, + TrivialTestCircuit<::Scalar>, + >::new( + OPCODE_1, + test_circuit2, + circuit_secondary, + num_augmented_circuit, + ); + + // generate the commitkey based on max num of constraints and reused it for all other augmented circuit + let circuit_public_params = vec![&running_claim1.params, &running_claim2.params]; + let (max_index_circuit, _) = circuit_public_params + .iter() + .enumerate() + .map(|(i, params)| -> (usize, usize) { (i, params.r1cs_shape_primary.num_cons) }) + .max_by(|(_, circuit_size1), (_, circuit_size2)| circuit_size1.cmp(circuit_size2)) + .unwrap(); + + let ck_primary = + gen_commitmentkey_by_r1cs(&circuit_public_params[max_index_circuit].r1cs_shape_primary); + let ck_secondary = + gen_commitmentkey_by_r1cs(&circuit_public_params[max_index_circuit].r1cs_shape_secondary); + + // set unified ck_primary, ck_secondary and update digest + running_claim1.params.ck_primary = Some(ck_primary.clone()); + running_claim1.params.ck_secondary = Some(ck_secondary.clone()); + + running_claim2.params.ck_primary = Some(ck_primary); + running_claim2.params.ck_secondary = Some(ck_secondary); + + let digest = compute_digest::>(&[ + running_claim1.get_publicparams(), + running_claim2.get_publicparams(), + ]); + + let num_steps = rom.len(); + let initial_program_counter = ::Scalar::from(0); + + // extend z0_primary/secondary with rom content + let mut z0_primary = vec![::Scalar::ONE]; + z0_primary.extend( + rom + .iter() + .map(|opcode| ::Scalar::from(*opcode as u64)), + ); + let mut z0_secondary = vec![::Scalar::ONE]; + z0_secondary.extend( + rom + .iter() + .map(|opcode| ::Scalar::from(*opcode as u64)), + ); + + let mut recursive_snark_option: Option> = None; + + for _ in 0..num_steps { + let program_counter = recursive_snark_option + .as_ref() + .map(|recursive_snark| recursive_snark.program_counter) + .unwrap_or_else(|| initial_program_counter); + let augmented_circuit_index = rom[u32::from_le_bytes( + // convert program counter from field to usize (only took le 4 bytes) + program_counter.to_repr().as_ref()[0..4].try_into().unwrap(), + ) as usize]; + + let mut recursive_snark = recursive_snark_option.unwrap_or_else(|| { + if augmented_circuit_index == OPCODE_0 { + RecursiveSNARK::iter_base_step( + &running_claim1, + digest, + program_counter, + augmented_circuit_index, + num_augmented_circuit, + &z0_primary, + &z0_secondary, + ) + .unwrap() + } else if augmented_circuit_index == OPCODE_1 { + RecursiveSNARK::iter_base_step( + &running_claim2, + digest, + program_counter, + augmented_circuit_index, + num_augmented_circuit, + &z0_primary, + &z0_secondary, + ) + .unwrap() + } else { + unimplemented!() + } + }); + + if augmented_circuit_index == OPCODE_0 { + let _ = recursive_snark + .prove_step(&running_claim1, &z0_primary, &z0_secondary) + .unwrap(); + let _ = recursive_snark + .verify(&running_claim1, &z0_primary, &z0_secondary) + .map_err(|err| { + print_constraints_name_on_error_index(err, &running_claim1, num_augmented_circuit) + }) + .unwrap(); + } else if augmented_circuit_index == OPCODE_1 { + let _ = recursive_snark + .prove_step(&running_claim2, &z0_primary, &z0_secondary) + .unwrap(); + let _ = recursive_snark + .verify(&running_claim2, &z0_primary, &z0_secondary) + .map_err(|err| { + print_constraints_name_on_error_index(err, &running_claim2, num_augmented_circuit) + }) + .unwrap(); + } + recursive_snark_option = Some(recursive_snark) + } + + assert!(recursive_snark_option.is_some()); + + // Now you can handle the Result using if let + let RecursiveSNARK { + zi_primary, + zi_secondary, + program_counter, + .. + } = &recursive_snark_option.unwrap(); + + println!("zi_primary: {:?}", zi_primary); + println!("zi_secondary: {:?}", zi_secondary); + println!("final program_counter: {:?}", program_counter); +} + +#[test] +fn test_trivial_nivc() { + type G1 = pasta_curves::pallas::Point; + type G2 = pasta_curves::vesta::Point; + + // Expirementing with selecting the running claims for nifs + test_trivial_nivc_with::(); +} + +// In the following we use 1 to refer to the primary, and 2 to refer to the secondary circuit +fn test_recursive_circuit_with( + primary_params: SuperNovaAugmentedCircuitParams, + secondary_params: SuperNovaAugmentedCircuitParams, + ro_consts1: ROConstantsCircuit, + ro_consts2: ROConstantsCircuit, + num_constraints_primary: usize, + num_constraints_secondary: usize, +) where + G1: Group::Scalar>, + G2: Group::Scalar>, +{ + // Both circuit1 and circuit2 are TrivialTestCircuit + + // Initialize the shape and ck for the primary + let step_circuit1 = TrivialTestCircuit::default(); + let arity1 = step_circuit1.arity(); + let circuit1: SuperNovaAugmentedCircuit<'_, G2, TrivialTestCircuit<::Base>> = + SuperNovaAugmentedCircuit::new(&primary_params, None, &step_circuit1, ro_consts1.clone(), 2); + let mut cs: ShapeCS = ShapeCS::new(); + if let Err(e) = circuit1.synthesize(&mut cs) { + panic!("{}", e) + } + let (shape1, ck1) = cs.r1cs_shape_with_commitmentkey(); + assert_eq!(cs.num_constraints(), num_constraints_primary); + + // Initialize the shape and ck for the secondary + let step_circuit2 = TrivialTestCircuit::default(); + let arity2 = step_circuit2.arity(); + let circuit2: SuperNovaAugmentedCircuit<'_, G1, TrivialTestCircuit<::Base>> = + SuperNovaAugmentedCircuit::new( + &secondary_params, + None, + &step_circuit2, + ro_consts2.clone(), + 2, + ); + let mut cs: ShapeCS = ShapeCS::new(); + if let Err(e) = circuit2.synthesize(&mut cs) { + panic!("{}", e) + } + let (shape2, ck2) = cs.r1cs_shape_with_commitmentkey(); + assert_eq!(cs.num_constraints(), num_constraints_secondary); + + // Execute the base case for the primary + let zero1 = <::Base as Field>::ZERO; + let z0 = vec![zero1; arity1]; + let mut cs1: SatisfyingAssignment = SatisfyingAssignment::new(); + let inputs1: SuperNovaAugmentedCircuitInputs<'_, G2> = SuperNovaAugmentedCircuitInputs::new( + scalar_as_base::(zero1), // pass zero for testing + zero1, + &z0, + None, + None, + None, + None, + zero1, + zero1, + ); + let step_circuit = TrivialTestCircuit::default(); + let circuit1: SuperNovaAugmentedCircuit<'_, G2, TrivialTestCircuit<::Base>> = + SuperNovaAugmentedCircuit::new(&primary_params, Some(inputs1), &step_circuit, ro_consts1, 2); + if let Err(e) = circuit1.synthesize(&mut cs1) { + panic!("{}", e) + } + let (inst1, witness1) = cs1.r1cs_instance_and_witness(&shape1, &ck1).unwrap(); + // Make sure that this is satisfiable + assert!(shape1.is_sat(&ck1, &inst1, &witness1).is_ok()); + + // Execute the base case for the secondary + let zero2 = <::Base as Field>::ZERO; + let z0 = vec![zero2; arity2]; + let mut cs2: SatisfyingAssignment = SatisfyingAssignment::new(); + let inputs2: SuperNovaAugmentedCircuitInputs<'_, G1> = SuperNovaAugmentedCircuitInputs::new( + scalar_as_base::(zero2), // pass zero for testing + zero2, + &z0, + None, + None, + Some(&inst1), + None, + zero2, + zero2, + ); + let step_circuit = TrivialTestCircuit::default(); + let circuit2: SuperNovaAugmentedCircuit<'_, G1, TrivialTestCircuit<::Base>> = + SuperNovaAugmentedCircuit::new( + &secondary_params, + Some(inputs2), + &step_circuit, + ro_consts2, + 2, + ); + if let Err(e) = circuit2.synthesize(&mut cs2) { + panic!("{}", e) + } + let (inst2, witness2) = cs2.r1cs_instance_and_witness(&shape2, &ck2).unwrap(); + // Make sure that it is satisfiable + assert!(shape2.is_sat(&ck2, &inst2, &witness2).is_ok()); +} + +#[test] +fn test_recursive_circuit() { + type G1 = pasta_curves::pallas::Point; + type G2 = pasta_curves::vesta::Point; + let params1 = SuperNovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, true); + let params2 = SuperNovaAugmentedCircuitParams::new(BN_LIMB_WIDTH, BN_N_LIMBS, false); + let ro_consts1: ROConstantsCircuit = PoseidonConstantsCircuit::new(); + let ro_consts2: ROConstantsCircuit = PoseidonConstantsCircuit::new(); + + test_recursive_circuit_with::(params1, params2, ro_consts1, ro_consts2, 9918, 12178); +} diff --git a/src/supernova/utils.rs b/src/supernova/utils.rs new file mode 100644 index 00000000..26bf583b --- /dev/null +++ b/src/supernova/utils.rs @@ -0,0 +1,66 @@ +use bellpepper_core::{boolean::Boolean, num::AllocatedNum, ConstraintSystem, SynthesisError}; + +use crate::{ + gadgets::{ + r1cs::{conditionally_select_alloc_relaxed_r1cs, AllocatedRelaxedR1CSInstance}, + utils::{alloc_const, alloc_num_equals, conditionally_select}, + }, + traits::Group, +}; + +// return the element matched index +// WARNING: there is no check for index out of bound. By default will return first one +// FIXME use api `try_result` https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.try_reduce to fine tune function logic once stable +// TODO optimize this part to have raw linear-combination on variables to achieve less constraints +pub fn get_from_vec_alloc_relaxed_r1cs::Base>>( + mut cs: CS, + a: &[AllocatedRelaxedR1CSInstance], + target_index: &AllocatedNum, +) -> Result<(AllocatedNum, AllocatedRelaxedR1CSInstance), SynthesisError> { + let mut a = a.iter().enumerate(); + let first = ( + alloc_const(cs.namespace(|| "i_const 0 allocated"), G::Base::from(0u64))?, + a.next() + .ok_or_else(|| SynthesisError::IncompatibleLengthVector("empty vec length".to_string()))? + .1 + .clone(), + ); + let selected = a.try_fold(first, |matched, (i, candidate)| { + let i_const = alloc_const( + cs.namespace(|| format!("i_const {:?} allocated", i)), + G::Base::from(i as u64), + )?; + let equal_bit = Boolean::from(alloc_num_equals( + cs.namespace(|| format!("check {:?} equal bit", i_const.get_value().unwrap())), + &i_const, + target_index, + )?); + let next_matched_index = conditionally_select( + cs.namespace(|| { + format!( + "select on index namespace {:?}", + i_const.get_value().unwrap() + ) + }), + &i_const, + &matched.0, + &equal_bit, + )?; + let next_matched_allocated = conditionally_select_alloc_relaxed_r1cs( + cs.namespace(|| { + format!( + "select on index namespace {:?}", + i_const.get_value().unwrap() + ) + }), + candidate, + &matched.1, + &equal_bit, + )?; + Ok::<(AllocatedNum, AllocatedRelaxedR1CSInstance), SynthesisError>(( + next_matched_index, + next_matched_allocated, + )) + })?; + Ok(selected) +} diff --git a/src/traits/circuit_supernova.rs b/src/traits/circuit_supernova.rs new file mode 100644 index 00000000..a6f1f6ba --- /dev/null +++ b/src/traits/circuit_supernova.rs @@ -0,0 +1,60 @@ +//! This module defines traits that a supernova step function must implement +use bellpepper_core::{num::AllocatedNum, ConstraintSystem, SynthesisError}; +use core::marker::PhantomData; +use ff::PrimeField; + +/// A helper trait for a step of the incremental computation for SuperNova (i.e., circuit for F) +pub trait StepCircuit: Send + Sync + Clone { + /// Return the the number of inputs or outputs of each step + /// (this method is called only at circuit synthesis time) + /// `synthesize` and `output` methods are expected to take as + /// input a vector of size equal to arity and output a vector of size equal to arity + fn arity(&self) -> usize; + + /// Sythesize the circuit for a computation step and return variable + /// that corresponds to the output of the step pc_{i+1} and z_{i+1} + fn synthesize>( + &self, + cs: &mut CS, + pc: &AllocatedNum, + z: &[AllocatedNum], + ) -> Result<(AllocatedNum, Vec>), SynthesisError>; +} + +/// A trivial step circuit that simply returns the input +#[derive(Clone, Debug, Default)] +pub struct TrivialTestCircuit { + _p: PhantomData, + rom_size: usize, +} + +impl TrivialTestCircuit +where + F: PrimeField, +{ + /// new TrivialTestCircuit + pub fn new(rom_size: usize) -> Self { + TrivialTestCircuit { + rom_size, + _p: PhantomData, + } + } +} + +impl StepCircuit for TrivialTestCircuit +where + F: PrimeField, +{ + fn arity(&self) -> usize { + 1 + self.rom_size // value + rom[].len() + } + + fn synthesize>( + &self, + _cs: &mut CS, + _pc_counter: &AllocatedNum, + z: &[AllocatedNum], + ) -> Result<(AllocatedNum, Vec>), SynthesisError> { + Ok((_pc_counter.clone(), z.to_vec())) + } +} diff --git a/src/traits/mod.rs b/src/traits/mod.rs index aae620ea..9cc9621e 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -234,5 +234,7 @@ impl> TranscriptReprTrait for &[T] { } pub mod circuit; +#[cfg(feature = "supernova")] +pub mod circuit_supernova; pub mod evaluation; pub mod snark;