Add external inputs logic to F function/circuit. Add an example of usage with external inputs too. (#78) * Add external inputs logic to F function/circuit. Add an example of usage with external inputs too. * Add examples run into CI
jump to
@@ -72,6 +72,18 @@ - name: Doctests # nextest does not support doc tests
run: | cargo test --doc + examples: + if: github.event.pull_request.draft == false + name: Run examples & examples tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + - name: Run examples tests + run: cargo test --examples + - name: Run examples + run: cargo run --release --example 2>&1 | grep -E '^ ' | xargs -n1 cargo run --release --example + fmt: if: github.event.pull_request.draft == false name: Rustfmt@@ -104,6 +116,7 @@ command: clippy
args: --all-targets --all-features -- -D warnings typos: + if: github.event.pull_request.draft == false name: Spell Check with Typos runs-on: ubuntu-latest steps:
@@ -0,0 +1,213 @@
+#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(clippy::upper_case_acronyms)] + +use ark_crypto_primitives::{ + crh::{ + poseidon::constraints::{CRHGadget, CRHParametersVar}, + poseidon::CRH, + CRHScheme, CRHSchemeGadget, + }, + sponge::{poseidon::PoseidonConfig, Absorb}, +}; +use ark_ff::PrimeField; +use ark_pallas::{constraints::GVar, Fr, Projective}; +use ark_r1cs_std::fields::fp::FpVar; +use ark_r1cs_std::{alloc::AllocVar, fields::FieldVar}; +use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::Zero; +use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; +use core::marker::PhantomData; +use std::time::Instant; + +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 folding_schemes::transcript::poseidon::poseidon_test_config; +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 2 elements, and z_{i+1} denotes the next state which +/// we get by applying the step. +/// +/// In this example we set the state to be the previous state together with an external input, and +/// the new state is an array which contains the new state and a zero which will be ignored. +/// +/// This is useful for example if we want to fold multiple verifications of signatures, where the +/// circuit F checks the signature and is folded for each of the signatures and public keys. To +/// keep things simpler, the following example does not verify signatures but does a similar +/// approach with a chain of hashes, where each iteration hashes the previous step output (z_i) +/// together with an external input (w_i). +/// +/// w_1 w_2 w_3 w_4 +/// │ │ │ │ +/// ▼ ▼ ▼ ▼ +/// ┌─┐ ┌─┐ ┌─┐ ┌─┐ +/// ─────►│F├────►│F├────►│F├────►│F├────► +/// z_1 └─┘ z_2 └─┘ z_3 └─┘ z_4 └─┘ z_5 +/// +/// +/// where each F is: +/// w_i +/// │ ┌────────────────────┐ +/// │ │FCircuit │ +/// │ │ │ +/// └────►│ h =Hash(z_i[0],w_i)│ +/// │ │ =Hash(v, w_i) │ +/// ────────►│ │ ├───────► +/// z_i=[v,0] │ └──►z_{i+1}=[h, 0] │ z_{i+1}=[h,0] +/// │ │ +/// └────────────────────┘ +/// +/// where each w_i value is set at the external_inputs array. +/// +/// The last state z_i is used together with the external input w_i as inputs to compute the new +/// state z_{i+1}. +/// The function F will output the new state in an array of two elements, where the second element +/// is a 0. In other words, z_{i+1} = [F([z_i, w_i]), 0], and the 0 will be replaced by w_{i+1} in +/// the next iteration, so z_{i+2} = [F([z_{i+1}, w_{i+1}]), 0]. +#[derive(Clone, Debug)] +pub struct ExternalInputsCircuits<F: PrimeField> +where + F: Absorb, +{ + _f: PhantomData<F>, + poseidon_config: PoseidonConfig<F>, + external_inputs: Vec<F>, +} +impl<F: PrimeField> FCircuit<F> for ExternalInputsCircuits<F> +where + F: Absorb, +{ + type Params = (PoseidonConfig<F>, Vec<F>); // where Vec<F> contains the external inputs + + fn new(params: Self::Params) -> Self { + Self { + _f: PhantomData, + poseidon_config: params.0, + external_inputs: params.1, + } + } + fn state_len(&self) -> usize { + 2 + } + + /// 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, i: usize, z_i: Vec<F>) -> Result<Vec<F>, Error> { + let input: [F; 2] = [z_i[0], self.external_inputs[i]]; + let h = CRH::<F>::evaluate(&self.poseidon_config, input).unwrap(); + Ok(vec![h, F::zero()]) + } + + /// generates the constraints for the step of F for the given z_i + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef<F>, + i: usize, + z_i: Vec<FpVar<F>>, + ) -> Result<Vec<FpVar<F>>, SynthesisError> { + let crh_params = + CRHParametersVar::<F>::new_constant(cs.clone(), self.poseidon_config.clone())?; + let external_inputVar = + FpVar::<F>::new_witness(cs.clone(), || Ok(self.external_inputs[i])).unwrap(); + let input: [FpVar<F>; 2] = [z_i[0].clone(), external_inputVar.clone()]; + let h = CRHGadget::<F>::evaluate(&crh_params, &input)?; + Ok(vec![h, FpVar::<F>::zero()]) + } +} + +/// cargo test --example external_inputs +#[cfg(test)] +pub mod tests { + use super::*; + use ark_r1cs_std::R1CSVar; + use ark_relations::r1cs::ConstraintSystem; + + // test to check that the ExternalInputsCircuits computes the same values inside and outside the circuit + #[test] + fn test_f_circuit() { + let poseidon_config = poseidon_test_config::<Fr>(); + + let cs = ConstraintSystem::<Fr>::new_ref(); + + let circuit = ExternalInputsCircuits::<Fr>::new((poseidon_config, vec![Fr::from(3_u32)])); + let z_i = vec![Fr::from(1_u32), Fr::zero()]; + + let z_i1 = circuit.step_native(0, z_i.clone()).unwrap(); + + let z_iVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i)).unwrap(); + let computed_z_i1Var = circuit + .generate_step_constraints(cs.clone(), 0, z_iVar.clone()) + .unwrap(); + assert_eq!(computed_z_i1Var.value().unwrap(), z_i1); + } +} + +/// cargo run --release --example external_inputs +fn main() { + let num_steps = 5; + let initial_state = vec![Fr::from(1_u32), Fr::zero()]; + + // set the external inputs to be used at each folding step + let external_inputs = vec![ + Fr::from(3_u32), + Fr::from(33_u32), + Fr::from(73_u32), + Fr::from(103_u32), + Fr::from(125_u32), + ]; + assert_eq!(external_inputs.len(), num_steps); + + let poseidon_config = poseidon_test_config::<Fr>(); + let F_circuit = ExternalInputsCircuits::<Fr>::new((poseidon_config, external_inputs)); + + println!("Prepare Nova ProverParams & VerifierParams"); + let (prover_params, verifier_params) = + test_nova_setup::<ExternalInputsCircuits<Fr>>(F_circuit.clone()); + + /// 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, + ExternalInputsCircuits<Fr>, + Pedersen<Projective>, + Pedersen<Projective2>, + >; + + 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()); + } + println!( + "state at last step (after {} iterations): {:?}", + num_steps, + folding_scheme.state() + ); + + let (running_instance, incoming_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, + incoming_instance, + cyclefold_instance, + ) + .unwrap(); +}
@@ -35,13 +35,13 @@
fn new(_params: Self::Params) -> Self { Self { _f: PhantomData } } - fn state_len(self) -> usize { + 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<F>) -> Result<Vec<F>, Error> { + fn step_native(&self, _i: usize, z_i: Vec<F>) -> Result<Vec<F>, 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);@@ -53,8 +53,9 @@ }
/// generates the constraints for the step of F for the given z_i fn generate_step_constraints( - self, + &self, cs: ConstraintSystemRef<F>, + _i: usize, z_i: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError> { let four = FpVar::<F>::new_constant(cs.clone(), F::from(4u32))?;@@ -74,12 +75,12 @@ /// cargo test --example multi_inputs
#[cfg(test)] pub mod tests { use super::*; - use ark_r1cs_std::alloc::AllocVar; + use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; 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() { + fn test_f_circuit() { let cs = ConstraintSystem::<Fr>::new_ref(); let circuit = MultiInputsFCircuit::<Fr>::new(());@@ -91,11 +92,11 @@ Fr::from(1_u32),
Fr::from(1_u32), ]; - let z_i1 = circuit.step_native(z_i.clone()).unwrap(); + let z_i1 = circuit.step_native(0, z_i.clone()).unwrap(); let z_iVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i)).unwrap(); let computed_z_i1Var = circuit - .generate_step_constraints(cs.clone(), z_iVar.clone()) + .generate_step_constraints(cs.clone(), 0, z_iVar.clone()) .unwrap(); assert_eq!(computed_z_i1Var.value().unwrap(), z_i1); }
@@ -41,13 +41,13 @@
fn new(_params: Self::Params) -> Self { Self { _f: PhantomData } } - fn state_len(self) -> usize { + 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} - fn step_native(self, z_i: Vec<F>) -> Result<Vec<F>, Error> { + fn step_native(&self, _i: usize, z_i: Vec<F>) -> Result<Vec<F>, Error> { let out_bytes = Sha256::evaluate(&(), z_i[0].into_bigint().to_bytes_le()).unwrap(); let out: Vec<F> = out_bytes.to_field_elements().unwrap();@@ -56,8 +56,9 @@ }
/// generates the constraints for the step of F for the given z_i fn generate_step_constraints( - self, + &self, _cs: ConstraintSystemRef<F>, + _i: usize, z_i: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError> { let unit_var = UnitVar::default();@@ -71,22 +72,22 @@ /// cargo test --example sha256
#[cfg(test)] pub mod tests { use super::*; - use ark_r1cs_std::alloc::AllocVar; + use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; use ark_relations::r1cs::ConstraintSystem; // test to check that the Sha256FCircuit computes the same values inside and outside the circuit #[test] - fn test_sha256_f_circuit() { + fn test_f_circuit() { let cs = ConstraintSystem::<Fr>::new_ref(); let circuit = Sha256FCircuit::<Fr>::new(()); let z_i = vec![Fr::from(1_u32)]; - let z_i1 = circuit.step_native(z_i.clone()).unwrap(); + let z_i1 = circuit.step_native(0, z_i.clone()).unwrap(); let z_iVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i)).unwrap(); let computed_z_i1Var = circuit - .generate_step_constraints(cs.clone(), z_iVar.clone()) + .generate_step_constraints(cs.clone(), 0, z_iVar.clone()) .unwrap(); assert_eq!(computed_z_i1Var.value().unwrap(), z_i1); }
@@ -96,7 +96,7 @@ ccs: &CCS<C>,
w: &Witness<C::ScalarField>, ) -> Result<(), Error> { // check that C is the commitment of w. Notice that this is not verifying a Pedersen - // opening, but checking that the Commmitment comes from committing to the witness. + // opening, but checking that the Commitment comes from committing to the witness. if self.C != Pedersen::<C>::commit(pedersen_params, &w.w, &w.r_w)? { return Err(Error::NotSatisfied); }
@@ -245,6 +245,7 @@ {
pub _gc2: PhantomData<GC2>, pub poseidon_config: PoseidonConfig<CF1<C1>>, pub i: Option<CF1<C1>>, + pub i_usize: Option<usize>, pub z_0: Option<Vec<C1::ScalarField>>, pub z_i: Option<Vec<C1::ScalarField>>, pub u_i: Option<CommittedInstance<C1>>,@@ -278,6 +279,7 @@ Self {
_gc2: PhantomData, poseidon_config: poseidon_config.clone(), i: None, + i_usize: None, z_0: None, z_i: None, u_i: None,@@ -349,7 +351,10 @@ self.poseidon_config.clone(),
)?; // get z_{i+1} from the F circuit - let z_i1 = self.F.generate_step_constraints(cs.clone(), z_i.clone())?; + let i_usize = self.i_usize.unwrap_or(0); + let z_i1 = self + .F + .generate_step_constraints(cs.clone(), i_usize, z_i.clone())?; let zero = FpVar::<CF1<C1>>::new_constant(cs.clone(), CF1::<C1>::zero())?; let is_not_basecase = i.is_neq(&zero)?;
@@ -508,7 +508,7 @@ let cubic_circuit = CubicFCircuit::<Fr>::new(());
let circuit = WrapperCircuit::<Fr, CubicFCircuit<Fr>> { FC: cubic_circuit, z_i: Some(z_i.clone()), - z_i1: Some(cubic_circuit.step_native(z_i).unwrap()), + z_i1: Some(cubic_circuit.step_native(0, z_i).unwrap()), }; test_relaxed_r1cs_gadget(circuit);@@ -551,7 +551,7 @@ let z_i = vec![Fr::from(5_u32)];
let circuit = WrapperCircuit::<Fr, CustomFCircuit<Fr>> { FC: custom_circuit, z_i: Some(z_i.clone()), - z_i1: Some(custom_circuit.step_native(z_i).unwrap()), + z_i1: Some(custom_circuit.step_native(0, z_i).unwrap()), }; test_relaxed_r1cs_gadget(circuit); }@@ -567,7 +567,7 @@ let z_i = vec![Fq::from(5_u32)];
let circuit = WrapperCircuit::<Fq, CustomFCircuit<Fq>> { FC: custom_circuit, z_i: Some(z_i.clone()), - z_i1: Some(custom_circuit.step_native(z_i).unwrap()), + z_i1: Some(custom_circuit.step_native(0, z_i).unwrap()), }; circuit.generate_constraints(cs.clone()).unwrap(); cs.finalize();
@@ -227,7 +227,7 @@ ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> {
let (prover_params, F_circuit) = prep_param; let (r1cs, cf_r1cs) = - get_r1cs::<C1, GC1, C2, GC2, FC>(&prover_params.poseidon_config, *F_circuit)?; + get_r1cs::<C1, GC1, C2, GC2, FC>(&prover_params.poseidon_config, F_circuit.clone())?; let verifier_params = VerifierParams::<C1, C2> { poseidon_config: prover_params.poseidon_config.clone(),@@ -244,7 +244,7 @@ let cs = ConstraintSystem::<C1::ScalarField>::new_ref();
let cs2 = ConstraintSystem::<C1::BaseField>::new_ref(); let augmented_F_circuit = - AugmentedFCircuit::<C1, C2, GC2, FC>::empty(&pp.poseidon_config, F); + AugmentedFCircuit::<C1, C2, GC2, FC>::empty(&pp.poseidon_config, F.clone()); let cf_circuit = CycleFoldCircuit::<C1, GC1>::empty(); augmented_F_circuit.generate_constraints(cs.clone())?;@@ -292,7 +292,14 @@ let augmented_F_circuit: AugmentedFCircuit<C1, C2, GC2, FC>;
let cfW_circuit: CycleFoldCircuit<C1, GC1>; let cfE_circuit: CycleFoldCircuit<C1, GC1>; - let z_i1 = self.F.step_native(self.z_i.clone())?; + if self.i > C1::ScalarField::from_le_bytes_mod_order(&std::usize::MAX.to_le_bytes()) { + return Err(Error::MaxStep); + } + let mut i_bytes: [u8; 8] = [0; 8]; + i_bytes.copy_from_slice(&self.i.into_bigint().to_bytes_le()[..8]); + let i_usize: usize = usize::from_le_bytes(i_bytes); + + let z_i1 = self.F.step_native(i_usize, self.z_i.clone())?; // compute T and cmT for AugmentedFCircuit let (T, cmT) = self.compute_cmT()?;@@ -327,13 +334,14 @@ augmented_F_circuit = AugmentedFCircuit::<C1, C2, GC2, FC> {
_gc2: PhantomData, poseidon_config: self.poseidon_config.clone(), i: Some(C1::ScalarField::zero()), // = i=0 - z_0: Some(self.z_0.clone()), // = z_i + i_usize: Some(0), + z_0: Some(self.z_0.clone()), // = z_i z_i: Some(self.z_i.clone()), u_i: Some(self.u_i.clone()), // = dummy U_i: Some(self.U_i.clone()), // = dummy U_i1: Some(U_i1.clone()), cmT: Some(cmT), - F: self.F, + F: self.F.clone(), x: Some(u_i1_x), cf1_u_i: None, cf2_u_i: None,@@ -399,13 +407,14 @@ augmented_F_circuit = AugmentedFCircuit::<C1, C2, GC2, FC> {
_gc2: PhantomData, poseidon_config: self.poseidon_config.clone(), i: Some(self.i), + i_usize: Some(i_usize), z_0: Some(self.z_0.clone()), z_i: Some(self.z_i.clone()), u_i: Some(self.u_i.clone()), U_i: Some(self.U_i.clone()), U_i1: Some(U_i1.clone()), cmT: Some(cmT), - F: self.F, + F: self.F.clone(), x: Some(u_i1_x), // cyclefold values cf1_u_i: Some(cfW_u_i.clone()),
@@ -10,7 +10,7 @@ /// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie.
/// inside the agmented F' function). /// The parameter z_i denotes the current state, and z_{i+1} denotes the next state after applying /// the step. -pub trait FCircuit<F: PrimeField>: Clone + Copy + Debug { +pub trait FCircuit<F: PrimeField>: Clone + Debug { type Params: Debug; /// returns a new FCircuit instance@@ -18,14 +18,15 @@ 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; + 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( // this method uses self, so that each FCircuit implementation (and different frontends) // can hold a state if needed to store data to compute the next state. - self, + &self, + i: usize, z_i: Vec<F>, ) -> Result<Vec<F>, Error>;@@ -33,8 +34,9 @@ /// generates the constraints for the step of F for the given z_i
fn generate_step_constraints( // this method uses self, so that each FCircuit implementation (and different frontends) // can hold a state if needed to store data to generate the constraints. - self, + &self, cs: ConstraintSystemRef<F>, + i: usize, z_i: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError>; }@@ -63,15 +65,16 @@ type Params = ();
fn new(_params: Self::Params) -> Self { Self { _f: PhantomData } } - fn state_len(self) -> usize { + fn state_len(&self) -> usize { 1 } - fn step_native(self, z_i: Vec<F>) -> Result<Vec<F>, Error> { + fn step_native(&self, _i: usize, z_i: Vec<F>) -> Result<Vec<F>, Error> { Ok(vec![z_i[0] * z_i[0] * z_i[0] + z_i[0] + F::from(5_u32)]) } fn generate_step_constraints( - self, + &self, cs: ConstraintSystemRef<F>, + _i: usize, z_i: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError> { let five = FpVar::<F>::new_constant(cs.clone(), F::from(5u32))?;@@ -97,10 +100,10 @@ _f: PhantomData,
n_constraints: params, } } - fn state_len(self) -> usize { + fn state_len(&self) -> usize { 1 } - fn step_native(self, z_i: Vec<F>) -> Result<Vec<F>, Error> { + fn step_native(&self, _i: usize, z_i: Vec<F>) -> Result<Vec<F>, Error> { let mut z_i1 = F::one(); for _ in 0..self.n_constraints - 1 { z_i1 *= z_i[0];@@ -108,8 +111,9 @@ }
Ok(vec![z_i1]) } fn generate_step_constraints( - self, + &self, cs: ConstraintSystemRef<F>, + _i: usize, z_i: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError> { let mut z_i1 = FpVar::<F>::new_witness(cs.clone(), || Ok(F::one()))?;@@ -143,7 +147,9 @@ })?;
let z_i1 = Vec::<FpVar<F>>::new_input(cs.clone(), || { Ok(self.z_i1.unwrap_or(vec![F::zero()])) })?; - let computed_z_i1 = self.FC.generate_step_constraints(cs.clone(), z_i.clone())?; + let computed_z_i1 = self + .FC + .generate_step_constraints(cs.clone(), 0, z_i.clone())?; computed_z_i1.enforce_equal(&z_i1)?; Ok(())@@ -173,7 +179,7 @@ let z_i = vec![Fr::from(5_u32)];
let wrapper_circuit = WrapperCircuit::<Fr, CustomFCircuit<Fr>> { FC: custom_circuit, z_i: Some(z_i.clone()), - z_i1: Some(custom_circuit.step_native(z_i).unwrap()), + z_i1: Some(custom_circuit.step_native(0, z_i).unwrap()), }; wrapper_circuit.generate_constraints(cs.clone()).unwrap(); assert_eq!(cs.num_constraints(), n_constraints);
@@ -66,6 +66,8 @@ #[error("Could not construct the Evaluation Domain")]
NewDomainFail, #[error("Feature '{0}' not supported yet")] NotSupportedYet(String), + #[error("max i-th step reached (usize limit reached)")] + MaxStep, #[error(transparent)] ProtoGalaxy(folding::protogalaxy::ProtoGalaxyError),