From 89d6067431435c53692ba087569a278c67265210 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Thu, 22 Feb 2024 13:54:54 +0100 Subject: [PATCH] Fix Nova multi-elements state (#73) * Fix Nova multi-elements state In the AugmentedFCircuit the default value for the state when no input is provided was `vec![F::zero()]`, which defaults to length `1`. So when having more than 1 element in the state, before even starting to fold, the circuit was already already failing. Additionally this commit adds an example for a circuit with a state of 5 elements. * abstract 'nova_setup' helper to avoid code duplication in examples * update example naming to 'MultiInputs' * rename nova_setup -> test_nova_setup to make it more explicit --- folding-schemes/examples/multi_inputs.rs | 156 ++++++++++++++++++ .../examples/{fold_sha256.rs => sha256.rs} | 49 +----- folding-schemes/examples/utils.rs | 49 ++++++ folding-schemes/src/folding/nova/circuits.rs | 8 +- folding-schemes/src/frontend/mod.rs | 10 ++ 5 files changed, 230 insertions(+), 42 deletions(-) create mode 100644 folding-schemes/examples/multi_inputs.rs rename folding-schemes/examples/{fold_sha256.rs => sha256.rs} (72%) create mode 100644 folding-schemes/examples/utils.rs diff --git a/folding-schemes/examples/multi_inputs.rs b/folding-schemes/examples/multi_inputs.rs new file mode 100644 index 0000000..4d5fc55 --- /dev/null +++ b/folding-schemes/examples/multi_inputs.rs @@ -0,0 +1,156 @@ +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(clippy::upper_case_acronyms)] + +use ark_ff::PrimeField; +use ark_r1cs_std::alloc::AllocVar; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; +use core::marker::PhantomData; +use std::time::Instant; + +use ark_pallas::{constraints::GVar, Fr, Projective}; +use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; + +use folding_schemes::commitment::pedersen::Pedersen; +use folding_schemes::folding::nova::Nova; +use folding_schemes::frontend::FCircuit; +use folding_schemes::{Error, FoldingScheme}; +mod utils; +use utils::test_nova_setup; + +/// This is the circuit that we want to fold, it implements the FCircuit trait. The parameter z_i +/// denotes the current state which contains 5 elements, and z_{i+1} denotes the next state which +/// we get by applying the step. +/// In this example we set z_i and z_{i+1} to have five elements, and at each step we do different +/// operations on each of them. +#[derive(Clone, Copy, Debug)] +pub struct MultiInputsFCircuit { + _f: PhantomData, +} +impl FCircuit for MultiInputsFCircuit { + type Params = (); + + fn new(_params: Self::Params) -> Self { + Self { _f: PhantomData } + } + fn state_len(self) -> usize { + 5 + } + + /// computes the next state values in place, assigning z_{i+1} into z_i, and computing the new + /// z_{i+1} + fn step_native(self, z_i: Vec) -> Result, Error> { + let a = z_i[0] + F::from(4_u32); + let b = z_i[1] + F::from(40_u32); + let c = z_i[2] * F::from(4_u32); + let d = z_i[3] * F::from(40_u32); + let e = z_i[4] + F::from(100_u32); + + Ok(vec![a, b, c, d, e]) + } + + /// generates the constraints for the step of F for the given z_i + fn generate_step_constraints( + self, + cs: ConstraintSystemRef, + z_i: Vec>, + ) -> Result>, SynthesisError> { + let four = FpVar::::new_constant(cs.clone(), F::from(4u32))?; + let fourty = FpVar::::new_constant(cs.clone(), F::from(40u32))?; + let onehundred = FpVar::::new_constant(cs.clone(), F::from(100u32))?; + let a = z_i[0].clone() + four.clone(); + let b = z_i[1].clone() + fourty.clone(); + let c = z_i[2].clone() * four; + let d = z_i[3].clone() * fourty; + let e = z_i[4].clone() + onehundred; + + Ok(vec![a, b, c, d, e]) + } +} + +/// cargo test --example multi_inputs +#[cfg(test)] +pub mod tests { + use super::*; + use ark_r1cs_std::alloc::AllocVar; + use ark_relations::r1cs::ConstraintSystem; + + // test to check that the MultiInputsFCircuit computes the same values inside and outside the circuit + #[test] + fn test_add_f_circuit() { + let cs = ConstraintSystem::::new_ref(); + + let circuit = MultiInputsFCircuit::::new(()); + let z_i = vec![ + Fr::from(1_u32), + Fr::from(1_u32), + Fr::from(1_u32), + Fr::from(1_u32), + Fr::from(1_u32), + ]; + + let z_i1 = circuit.step_native(z_i.clone()).unwrap(); + + let z_iVar = Vec::>::new_witness(cs.clone(), || Ok(z_i)).unwrap(); + let computed_z_i1Var = circuit + .generate_step_constraints(cs.clone(), z_iVar.clone()) + .unwrap(); + assert_eq!(computed_z_i1Var.value().unwrap(), z_i1); + } +} + +/// cargo run --release --example multi_inputs +fn main() { + let num_steps = 10; + let initial_state = vec![ + Fr::from(1_u32), + Fr::from(1_u32), + Fr::from(1_u32), + Fr::from(1_u32), + Fr::from(1_u32), + ]; + + let F_circuit = MultiInputsFCircuit::::new(()); + + println!("Prepare Nova ProverParams & VerifierParams"); + let (prover_params, verifier_params) = test_nova_setup::>(F_circuit); + + /// The idea here is that eventually we could replace the next line chunk that defines the + /// `type NOVA = Nova<...>` by using another folding scheme that fulfills the `FoldingScheme` + /// trait, and the rest of our code would be working without needing to be updated. + type NOVA = Nova< + Projective, + GVar, + Projective2, + GVar2, + MultiInputsFCircuit, + Pedersen, + Pedersen, + >; + + println!("Initialize FoldingScheme"); + let mut folding_scheme = NOVA::init(&prover_params, F_circuit, initial_state.clone()).unwrap(); + + // compute a step of the IVC + for i in 0..num_steps { + let start = Instant::now(); + folding_scheme.prove_step().unwrap(); + println!("Nova::prove_step {}: {:?}", i, start.elapsed()); + } + + let (running_instance, incomming_instance, cyclefold_instance) = folding_scheme.instances(); + + println!("Run the Nova's IVC verifier"); + NOVA::verify( + verifier_params, + initial_state.clone(), + folding_scheme.state(), // latest state + Fr::from(num_steps as u32), + running_instance, + incomming_instance, + cyclefold_instance, + ) + .unwrap(); +} diff --git a/folding-schemes/examples/fold_sha256.rs b/folding-schemes/examples/sha256.rs similarity index 72% rename from folding-schemes/examples/fold_sha256.rs rename to folding-schemes/examples/sha256.rs index 74ad626..4a17945 100644 --- a/folding-schemes/examples/fold_sha256.rs +++ b/folding-schemes/examples/sha256.rs @@ -20,10 +20,11 @@ use ark_pallas::{constraints::GVar, Fr, Projective}; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use folding_schemes::commitment::pedersen::Pedersen; -use folding_schemes::folding::nova::{get_r1cs, Nova, ProverParams, VerifierParams}; +use folding_schemes::folding::nova::Nova; use folding_schemes::frontend::FCircuit; -use folding_schemes::transcript::poseidon::poseidon_test_config; use folding_schemes::{Error, FoldingScheme}; +mod utils; +use utils::test_nova_setup; /// This is the circuit that we want to fold, it implements the FCircuit trait. /// The parameter z_i denotes the current state, and z_{i+1} denotes the next state which we get by @@ -40,6 +41,9 @@ impl FCircuit for Sha256FCircuit { fn new(_params: Self::Params) -> Self { Self { _f: PhantomData } } + fn state_len(self) -> usize { + 1 + } /// computes the next state values in place, assigning z_{i+1} into z_i, and computing the new /// z_{i+1} @@ -63,7 +67,7 @@ impl FCircuit for Sha256FCircuit { } } -/// cargo test --example simple +/// cargo test --example sha256 #[cfg(test)] pub mod tests { use super::*; @@ -88,42 +92,7 @@ pub mod tests { } } -// This method computes the Prover & Verifier parameters for the example. For a real world use case -// those parameters should be generated carefuly (both the PoseidonConfig and the PedersenParams) -#[allow(clippy::type_complexity)] -fn nova_setup>( - F_circuit: FC, -) -> ( - ProverParams, Pedersen>, - VerifierParams, -) { - let mut rng = ark_std::test_rng(); - let poseidon_config = poseidon_test_config::(); - - // get the CM & CF_CM len - let (r1cs, cf_r1cs) = - get_r1cs::(&poseidon_config, F_circuit).unwrap(); - let cm_len = r1cs.A.n_rows; - let cf_cm_len = cf_r1cs.A.n_rows; - - let pedersen_params = Pedersen::::new_params(&mut rng, cm_len); - let cf_pedersen_params = Pedersen::::new_params(&mut rng, cf_cm_len); - - let prover_params = - ProverParams::, Pedersen> { - poseidon_config: poseidon_config.clone(), - cm_params: pedersen_params, - cf_cm_params: cf_pedersen_params, - }; - let verifier_params = VerifierParams:: { - poseidon_config: poseidon_config.clone(), - r1cs, - cf_r1cs, - }; - (prover_params, verifier_params) -} - -/// cargo run --release --example fold_sha256 +/// cargo run --release --example sha256 fn main() { let num_steps = 10; let initial_state = vec![Fr::from(1_u32)]; @@ -131,7 +100,7 @@ fn main() { let F_circuit = Sha256FCircuit::::new(()); println!("Prepare Nova ProverParams & VerifierParams"); - let (prover_params, verifier_params) = nova_setup::>(F_circuit); + let (prover_params, verifier_params) = test_nova_setup::>(F_circuit); /// The idea here is that eventually we could replace the next line chunk that defines the /// `type NOVA = Nova<...>` by using another folding scheme that fulfills the `FoldingScheme` diff --git a/folding-schemes/examples/utils.rs b/folding-schemes/examples/utils.rs new file mode 100644 index 0000000..b5fee5e --- /dev/null +++ b/folding-schemes/examples/utils.rs @@ -0,0 +1,49 @@ +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(clippy::upper_case_acronyms)] +#![allow(dead_code)] +use ark_pallas::{constraints::GVar, Fr, Projective}; +use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; + +use folding_schemes::commitment::pedersen::Pedersen; +use folding_schemes::folding::nova::{get_r1cs, ProverParams, VerifierParams}; +use folding_schemes::frontend::FCircuit; +use folding_schemes::transcript::poseidon::poseidon_test_config; + +// This method computes the Prover & Verifier parameters for the example. +// Warning: this method is only for testing purposes. For a real world use case those parameters +// should be generated carefuly (both the PoseidonConfig and the PedersenParams). +#[allow(clippy::type_complexity)] +pub(crate) fn test_nova_setup>( + F_circuit: FC, +) -> ( + ProverParams, Pedersen>, + VerifierParams, +) { + let mut rng = ark_std::test_rng(); + let poseidon_config = poseidon_test_config::(); + + // get the CM & CF_CM len + let (r1cs, cf_r1cs) = + get_r1cs::(&poseidon_config, F_circuit).unwrap(); + let cm_len = r1cs.A.n_rows; + let cf_cm_len = cf_r1cs.A.n_rows; + + let pedersen_params = Pedersen::::new_params(&mut rng, cm_len); + let cf_pedersen_params = Pedersen::::new_params(&mut rng, cf_cm_len); + + let prover_params = + ProverParams::, Pedersen> { + poseidon_config: poseidon_config.clone(), + cm_params: pedersen_params, + cf_cm_params: cf_pedersen_params, + }; + let verifier_params = VerifierParams:: { + poseidon_config: poseidon_config.clone(), + r1cs, + cf_r1cs, + }; + (prover_params, verifier_params) +} +fn main() {} diff --git a/folding-schemes/src/folding/nova/circuits.rs b/folding-schemes/src/folding/nova/circuits.rs index 26885a5..a1a4302 100644 --- a/folding-schemes/src/folding/nova/circuits.rs +++ b/folding-schemes/src/folding/nova/circuits.rs @@ -310,10 +310,14 @@ where Ok(self.i.unwrap_or_else(CF1::::zero)) })?; let z_0 = Vec::>>::new_witness(cs.clone(), || { - Ok(self.z_0.unwrap_or(vec![CF1::::zero()])) + Ok(self + .z_0 + .unwrap_or(vec![CF1::::zero(); self.F.state_len()])) })?; let z_i = Vec::>>::new_witness(cs.clone(), || { - Ok(self.z_i.unwrap_or(vec![CF1::::zero()])) + Ok(self + .z_i + .unwrap_or(vec![CF1::::zero(); self.F.state_len()])) })?; let u_dummy_native = CommittedInstance::::dummy(1); diff --git a/folding-schemes/src/frontend/mod.rs b/folding-schemes/src/frontend/mod.rs index fced191..890f3a9 100644 --- a/folding-schemes/src/frontend/mod.rs +++ b/folding-schemes/src/frontend/mod.rs @@ -16,6 +16,10 @@ pub trait FCircuit: Clone + Copy + Debug { /// returns a new FCircuit instance fn new(params: Self::Params) -> Self; + /// returns the number of elements in the state of the FCircuit, which corresponds to the + /// FCircuit inputs. + fn state_len(self) -> usize; + /// computes the next state values in place, assigning z_{i+1} into z_i, and computing the new /// z_{i+1} fn step_native( @@ -59,6 +63,9 @@ pub mod tests { fn new(_params: Self::Params) -> Self { Self { _f: PhantomData } } + fn state_len(self) -> usize { + 1 + } fn step_native(self, z_i: Vec) -> Result, Error> { Ok(vec![z_i[0] * z_i[0] * z_i[0] + z_i[0] + F::from(5_u32)]) } @@ -90,6 +97,9 @@ pub mod tests { n_constraints: params, } } + fn state_len(self) -> usize { + 1 + } fn step_native(self, z_i: Vec) -> Result, Error> { let mut z_i1 = F::one(); for _ in 0..self.n_constraints - 1 {