update to newer sonobe's interface
@@ -27,21 +27,14 @@ Groth16<Bn254>, // here we define the Snark to use in the decider
NOVA, // here we define the FoldingScheme to use >; -// generate Groth16 setup -let circuit = DeciderEthCircuit::< - Projective, - GVar, - Projective2, - GVar2, - Pedersen<Projective>, - Pedersen<Projective2>, ->::from_nova::<CubicFCircuit<Fr>>(nova.clone()); let mut rng = rand::rngs::OsRng; -let (pk, vk) = - Groth16::<Bn254>::circuit_specific_setup(circuit.clone(), &mut rng).unwrap(); +// prepare the Decider prover & verifier params for the given nova_params and nova instance. This involves generating the Groth16 and KZG10 setup +let (decider_pp, decider_vp) = DECIDER::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); // decider proof generation -let decider_pp = (poseidon_config.clone(), g16_pk, kzg_pk); -let proof = DECIDER::prove(decider_pp, rng, nova.clone()).unwrap(); +let proof = DECIDER::prove(rng, decider_pp, nova.clone()).unwrap(); + ``` + +As in the previous sections, you can find a full examples with all the code at [sonobe/examples](https://github.com/privacy-scaling-explorations/sonobe/tree/main/examples).
@@ -15,7 +15,6 @@ Groth16<Bn254>,
NOVA, >; -let decider_vp = (g16_vk, kzg_vk); let verified = DECIDER::verify( decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, proof, )
@@ -3,10 +3,10 @@
We plug our `FCircuit` into the library: ```rust -// The idea here is that eventually we could replace the next line chunk that defines the +// The idea here is that 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< +type F = Nova< Projective, GVar, Projective2,@@ -14,6 +14,7 @@ GVar2,
Sha256FCircuit<Fr>, KZG<'static, Bn254>, Pedersen<Projective2>, + false, >; let num_steps = 10;@@ -21,25 +22,35 @@ let initial_state = vec![Fr::from(1_u32)];
let F_circuit = Sha256FCircuit::<Fr>::new(()); +let poseidon_config = poseidon_canonical_config::<Fr>(); +let mut rng = rand::rngs::OsRng; + println!("Prepare Nova ProverParams & VerifierParams"); -let (prover_params, verifier_params) = nova_setup::<Sha256FCircuit<Fr>>(F_circuit); +let nova_preprocess_params = PreprocessorParam::new(poseidon_config, F_circuit); +let nova_params = FS::preprocess(&mut rng, &nova_preprocess_params).unwrap(); + println!("Initialize FoldingScheme"); -let mut folding_scheme = NOVA::init(&prover_params, F_circuit, initial_state.clone()).unwrap(); +let mut folding_scheme = FS::init(&nova_params, F_circuit, initial_state.clone()).unwrap(); + // compute a step of the IVC for i in 0..num_steps { let start = Instant::now(); - // here we pass an empty vec since it does not use external_inputs - folding_scheme.prove_step(vec![]).unwrap(); + // - 2nd parameter: here we pass an empty vec since the FCircuit that we're + // using does not use external_inputs + // - 3rd parameter: is for schemes that support folding more than 2 + // instances at each fold, such as HyperNova. Since we're using Nova, we just + // set it to 'None' + folding_scheme.prove_step(rng, vec![], None).unwrap(); println!("Nova::prove_step {}: {:?}", i, start.elapsed()); } let (running_instance, incoming_instance, cyclefold_instance) = folding_scheme.instances(); println!("Run the Nova's IVC verifier"); -NOVA::verify( - verifier_params, +FS::verify( + nova_params.1, initial_state, folding_scheme.state(), // latest state Fr::from(num_steps as u32),@@ -49,3 +60,26 @@ cyclefold_instance,
) .unwrap(); ``` + +<br> + +Now imagine that we want to switch the folding scheme being used. Is as simple as replacing `FS` by: + +```rust +type FS = HyperNova< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit<Fr>, + KZG<'static, Bn254>, + Pedersen<Projective2>, + 1, 1, false, +>; +``` +and then adapting the `folding_scheme.prove_step(...)` call accordingly. + + +<br><br> + +As in the previous sections, you can find a full examples with all the code at [sonobe/examples](https://github.com/privacy-scaling-explorations/sonobe/tree/main/examples).
@@ -4,7 +4,7 @@ Let's walk through different simple examples implementing the `FCircuit` trait. By the end of this section, you will hopefully be familiar with how to integrate an `arkworks` circuit into sonobe.
You can find most of the following examples with the rest of code to run them at the [`examples`](https://github.com/privacy-scaling-explorations/sonobe/tree/main/examples) directory of the Sonobe repo. -## Cubic circuit +## Cubic circuit example This first example implements the `FCircuit` trait for the R1CS example circuit from [Vitalik's post](https://www.vitalik.ca/general/2016/12/10/qap.html), which checks $x^3 + x + 5 == y$. $z_i$ is used as $x$, and $z_{i+1}$ is used as $y$, and at the next step, $z_{i+1}$ will be assigned to $z_i$, and a new $z_{i+1}$ will be computted.@@ -16,8 +16,8 @@ _f: PhantomData<F>,
} impl<F: PrimeField> FCircuit<F> for CubicFCircuit<F> { type Params = (); - fn new(_params: Self::Params) -> Self { - Self { _f: PhantomData } + fn new(_params: Self::Params) -> Result<Self, Error> { + Ok(Self { _f: PhantomData }) } fn state_len(&self) -> usize { 1@@ -25,7 +25,12 @@ }
fn external_inputs_len(&self) -> usize { 0 } - fn step_native(&self, _i: usize, z_i: Vec<F>, _external_inputs: Vec<F>) -> Result<Vec<F>, Error> { + fn step_native( + &self, + _i: usize, + z_i: Vec<F>, + _external_inputs: 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(@@ -44,9 +49,9 @@ }
``` -## Folding a simple circuit +## Multiple inputs circuit example -The circuit we will fold has a state of 5 public elements. At each step, we will want the circuit to compute the next state by: +The following example has a state of 5 public elements. At each step, we will want the circuit to compute the next state by: 1. adding 4 to the first element 2. adding 40 to the second element@@ -54,7 +59,6 @@ 3. multiplying the third element by 4
4. multiplying the fourth element by 40 5. adding 100 to the fifth element -Let's implement this now: ```rust // Define a struct that will be our circuit. This struct will implement the FCircuit trait.@@ -62,35 +66,37 @@ #[derive(Clone, Copy, Debug)]
pub struct MultiInputsFCircuit<F: PrimeField> { _f: PhantomData<F>, } - -// Implement the FCircuit trait for the struct impl<F: PrimeField> FCircuit<F> for MultiInputsFCircuit<F> { type Params = (); - fn new(_params: Self::Params) -> Self { - Self { _f: PhantomData } + fn new(_params: Self::Params) -> Result<Self, Error> { + Ok(Self { _f: PhantomData }) } - fn state_len(&self) -> usize { - 5 // This circuit has 5 inputs + 5 // since the circuit has 5 inputs } fn external_inputs_len(&self) -> usize { 0 } - // Computes the next state values in place, assigning z_{i+1} into z_i, and computing the new z_{i+1} - // We want the `step_native` method to implement the same logic as the `generate_step_constraints` method - fn step_native(&self, _i: usize, z_i: Vec<F>, _external_inputs: Vec<F>) -> Result<Vec<F>, Error> { + /// 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>, + _external_inputs: 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); 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]) // The length of the returned vector should match `state_len` + Ok(vec![a, b, c, d, e]) } - /// Generates R1CS constraints for the step of F for the given z_i + /// generates the constraints for the step of F for the given z_i fn generate_step_constraints( &self, cs: ConstraintSystemRef<F>,@@ -98,7 +104,6 @@ _i: usize,
z_i: Vec<FpVar<F>>, _external_inputs: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError> { - // Implementing the circuit constraints let four = FpVar::<F>::new_constant(cs.clone(), F::from(4u32))?; let forty = FpVar::<F>::new_constant(cs.clone(), F::from(40u32))?; let onehundred = FpVar::<F>::new_constant(cs.clone(), F::from(100u32))?;@@ -108,64 +113,10 @@ let c = z_i[2].clone() * four;
let d = z_i[3].clone() * forty; let e = z_i[4].clone() + onehundred; - Ok(vec![a, b, c, d, e]) // The length of the returned vector should match `state_len` - } -} -``` - -## Folding a `Sha256` circuit - -We will fold a simple `Sha256` circuit. The circuit has a state of 1 public element. At each step, we will want the circuit to compute the next state by applying the `Sha256` function to the current state. - -Note that the logic here is also very similar to the previous example: write a struct that will hold the circuit, implement the `FCircuit` trait for the struct, ensure that the length of the state is correct, and implement the `step_native` and `generate_step_constraints` methods. - -Note: to simplify things for the example, only the first byte outputted by the sha256 is used for the next state $z_{i+1}$. - -```rust -// Define a struct that will be our circuit. This struct will implement the FCircuit trait. -#[derive(Clone, Copy, Debug)] -pub struct Sha256FCircuit<F: PrimeField> { - _f: PhantomData<F>, -} - -impl<F: PrimeField> FCircuit<F> for Sha256FCircuit<F> { - type Params = (); - - fn new(_params: Self::Params) -> Self { - Self { _f: PhantomData } - } - fn state_len(&self) -> usize { - 1 - } - fn external_inputs_len(&self) -> usize { - 0 - } - - /// 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>, _external_inputs: 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(); - - Ok(vec![out[0]]) - } - - /// 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>>, - _external_inputs: Vec<FpVar<F>>, - ) -> Result<Vec<FpVar<F>>, SynthesisError> { - let unit_var = UnitVar::default(); - let out_bytes = Sha256Gadget::evaluate(&unit_var, &z_i[0].to_bytes()?)?; - let out = out_bytes.0.to_constraint_field()?; - Ok(vec![out[0].clone()]) + Ok(vec![a, b, c, d, e]) } } ``` - ## Using external inputs@@ -200,6 +151,15 @@
The last state $z_i$ is used together with the external input w_i as inputs to compute the new state $z_{i+1}$. ```rust +use ark_crypto_primitives::{ + crh::{ + poseidon::constraints::{CRHGadget, CRHParametersVar}, + poseidon::CRH, + CRHScheme, CRHSchemeGadget, + }, + sponge::{poseidon::PoseidonConfig, Absorb}, +}; + #[derive(Clone, Debug)] pub struct ExternalInputsCircuits<F: PrimeField> where@@ -208,17 +168,17 @@ {
_f: PhantomData<F>, poseidon_config: PoseidonConfig<F>, } -impl<F: PrimeField> FCircuit<F> for ExternalInputsCircuits<F> +impl<F: PrimeField> FCircuit<F> for ExternalInputsCircuit<F> where F: Absorb, { - type Params = (PoseidonConfig<F>); + type Params = PoseidonConfig<F>; - fn new(params: Self::Params) -> Self { - Self { + fn new(params: Self::Params) -> Result<Self, Error> { + Ok(Self { _f: PhantomData, - poseidon_config: params.0, - } + poseidon_config: params, + }) } fn state_len(&self) -> usize { 1@@ -229,7 +189,12 @@ }
/// computes the next state value for the step of F for the given z_i and external_inputs /// z_{i+1} - fn step_native(&self, i: usize, z_i: Vec<F>, external_inputs: Vec<F>) -> Result<Vec<F>, Error> { + fn step_native( + &self, + _i: usize, + z_i: Vec<F>, + external_inputs: Vec<F>, + ) -> Result<Vec<F>, Error> { let hash_input: [F; 2] = [z_i[0], external_inputs[0]]; let h = CRH::<F>::evaluate(&self.poseidon_config, hash_input).unwrap(); Ok(vec![h])@@ -240,7 +205,7 @@ /// z_i and external_inputs
fn generate_step_constraints( &self, cs: ConstraintSystemRef<F>, - i: usize, + _i: usize, z_i: Vec<FpVar<F>>, external_inputs: Vec<FpVar<F>>, ) -> Result<Vec<FpVar<F>>, SynthesisError> {@@ -252,3 +217,8 @@ Ok(vec![h])
} } ``` + +<br><br> + +# Complete examples +You can find the complete examples with all the imports at [sonobe/examples](https://github.com/privacy-scaling-explorations/sonobe/tree/main/examples).
@@ -8,25 +8,33 @@
```c template FCircuit(ivc_state_len, aux_inputs_len) { signal input ivc_input[ivc_state_len]; // IVC state - signal input external_inputs[aux_inputs_len]; // not state, + signal input external_inputs[aux_inputs_len]; // external inputs, not part of the folding state signal output ivc_output[ivc_state_len]; // next IVC state - // [...] + // here it goes the Circom circuit logic } component main {public [ivc_input]} = Example(); ``` The `ivc_input` is the array that defines the initial state, and the `ivc_output` is the array that defines the output state after the step. Both need to be of the same size. The `external_inputs` array expects auxiliary input values. -So for example, the following circuit does the traditional example at each step, which proves knowledge of $x$ such that $y==x^3 + x + e_0 + e_1$ for a known $y$ ($e_i$ are the `external_inputs[i]`): + +In the following image, the `ivc_input`=$z_i$, the `external_inputs`=$w_i$, and the `ivc_output`=$z_{i+1}$, and $F$ is the logic of our Circom circuit: +<p align="center"> + <img src="../imgs/folding-main-idea-diagram.png" style="width:70%;" /> +</p> + +<br> + +So for example, the following circuit proves (at each folding step) knowledge of $x$ such that $y==x^3 + x + e_0 + e_1$ for a known $y$ ($e_i$ are the `external_inputs[i]`): ```c pragma circom 2.0.3; template CubicCircuit() { signal input ivc_input[1]; // IVC state - signal input external_inputs[2]; // not state + signal input external_inputs[2]; // not part of the state signal output ivc_output[1]; // next IVC state@@ -41,47 +49,61 @@
component main {public [ivc_input]} = CubicCircuit(); ``` +<br><br> + Once your `circom` circuit is ready, you can instantiate it with Sonobe. To do this, you will need the `struct CircomFCircuit`. ```rust // we load our circom compiled R1CS, along with the witness wasm calculator let r1cs_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit.r1cs"); - let wasm_path = - PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); -let mut circom_fcircuit = CircomFCircuit::<Fr>::new((r1cs_path, wasm_path, 1, 2)).unwrap(); // state_len:1, external_inputs_len:2 +let wasm_path = + PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); +let f_circuit_params = (r1cs_path, wasm_path, 1, 2); // state_len:1, external_inputs_len:2 +let f_circuit = CircomFCircuit::<Fr>::new(f_circuit_params).unwrap(); -// to speed things up, you can define a custom step function to avoid defaulting to the snarkjs witness calculator + +// [optional] to speed things up, you can define a custom step function to avoid +// defaulting to the snarkjs witness calculator for the native computations, +// which would be slower than rust native operations circom_fcircuit.set_custom_step_native(Rc::new(|_i, z_i, _external| { let z = z_i[0]; Ok(vec![z * z * z + z + Fr::from(5)]) })); -// we initialize the required folding schemes parameters, the folding scheme and the final decider that we want to use -let (fs_prover_params, kzg_vk, g16_pk, g16_vk) = - init_ivc_and_decider_params::<CircomFCircuit<Fr>>(f_circuit.clone()); - -pub type NOVA = Nova<G1, GVar, G2, GVar2, CircomFCircuit<Fr>, KZG<'static, Bn254>, Pedersen<G2>>; -pub type DECIDERETH_FCircuit = DeciderEth< - G1, - GVar, - G2, - GVar2, - CircomFCircuit<Fr>, - KZG<'static, Bn254>, - Pedersen<G2>, - Groth16<Bn254>, - NOVA, +pub type N = + Nova<G1, GVar, G2, GVar2, CircomFCircuit<Fr>, KZG<'static, Bn254>, Pedersen<G2>, false>; +pub type D = DeciderEth< + G1, + GVar, + G2, + GVar2, + CircomFCircuit<Fr>, + KZG<'static, Bn254>, + Pedersen<G2>, + Groth16<Bn254>, + N, >; -// initialize the folding scheme engine, in our case we use Nova -let mut nova = NOVA::init(&fs_prover_params, f_circuit.clone(), z_0).unwrap(); +let poseidon_config = poseidon_canonical_config::<Fr>(); +let mut rng = rand::rngs::OsRng; + +// prepare the Nova prover & verifier params +let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); +let nova_params = N::preprocess(&mut rng, &nova_preprocess_params).unwrap(); + +// initialize the folding scheme engine, in this case we use Nova +let mut nova = N::init(&nova_params, f_circuit.clone(), z_0).unwrap(); // run n steps of the folding iteration for (i, external_inputs_at_step) in external_inputs.iter().enumerate() { let start = Instant::now(); - nova.prove_step(external_inputs_at_step.clone()).unwrap(); + // the last parameter at 'nova.prove_step()' is for schemes that support + // folding more than 2 instances at each fold, such as HyperNova. Since + // we're using Nova, we just set it to 'None' + nova.prove_step(rng, external_inputs_at_step.clone(), None) + .unwrap(); println!("Nova::prove_step {}: {:?}", i, start.elapsed()); } ``` -You can find an example for using Nova over a circom circuit [here](https://github.com/privacy-scaling-explorations/sonobe/blob/main/examples/circom_full_flow.rs). +You can find a full example using Nova to fold a Circom circuit at [sonobe/examples/circom_full_flow.rs](https://github.com/privacy-scaling-explorations/sonobe/blob/main/examples/circom_full_flow.rs).
@@ -1,6 +1,12 @@
# Frontend -The frontend interface allows to define the circuit to be folded. The currently available frontends are [circom](https://github.com/iden3/circom) and [arkworks](https://github.com/arkworks-rs/r1cs-std). We will show here how to define a circuit using `arkworks`. +The frontend interface allows to define the circuit to be folded. The currently available frontends are: +- [arkworks](https://github.com/arkworks-rs/r1cs-std) +- [Circom](https://github.com/iden3/circom) +- [Noir](https://noir-lang.org/) +- [Noname](https://github.com/zksecurity/noname) + +Defining a circuit to be folded is as simple as fulfilling the `FCircuit` trait interface. Henceforth, integrating a new zk circuits language into Sonobe, can be done by building a wrapper on top of it that satisfies the `FCircuit` trait. # The `FCircuit` trait@@ -14,18 +20,18 @@ /// the step.
pub trait FCircuit<F: PrimeField>: Clone + Debug { type Params: Debug; - /// Returns a new FCircuit instance - fn new(params: Self::Params) -> Self; + /// returns a new FCircuit instance + fn new(params: Self::Params) -> Result<Self, Error>; - /// Returns the number of elements in the state of the FCircuit, which corresponds to the + /// returns the number of elements in the state of the FCircuit, which corresponds to the /// FCircuit inputs. fn state_len(&self) -> usize; - + /// returns the number of elements in the external inputs used by the FCircuit. External inputs /// are optional, and in case no external inputs are used, this method should return 0. fn external_inputs_len(&self) -> usize; - /// Computes the next state values in place, assigning z_{i+1} into z_i, and computing the new + /// 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)@@ -36,7 +42,7 @@ z_i: Vec<F>,
external_inputs: Vec<F>, // inputs that are not part of the state ) -> Result<Vec<F>, Error>; - /// Generates the constraints for the step of F for the given z_i + /// 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.
@@ -5,3 +5,9 @@ - Define a circuit to be folded using a frontend such as [`circom`](https://github.com/iden3/circom) or [arkworks](https://github.com/arkworks-rs/r1cs-std).
- Fold the circuit using one of the folding schemes - Generate a final Decider proof - Verify the Decider proof, and in Ethereum case, generate a Solidity verifier + +<br><br> + +<p align="center"> + <img src="../imgs/sonobe-lib-pipeline.png" /> +</p>
@@ -30,7 +30,7 @@ )
.unwrap(); // prepare the setup params for the solidity verifier -let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((g16_vk, kzg_vk, f_circuit.state_len())); +let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, f_circuit.state_len())); // generate the solidity code let decider_solidity_code = get_decider_template_for_cyclefold_decider(nova_cyclefold_vk);@@ -43,6 +43,7 @@ let (_, output) = evm.call(verifier_address, calldata.clone());
assert_eq!(*output.last().unwrap(), 1); // save smart contract and the calldata +println!("storing nova-verifier.sol and the calldata into files"); use std::fs; fs::write( "./examples/nova-verifier.sol", decider_solidity_code.clone()).unwrap(); fs::write("./examples/solidity-calldata.calldata", calldata.clone()).unwrap();