Browse Source

update to newer sonobe's interface

main
arnaucube 1 year ago
parent
commit
2dadf599d2
8 changed files with 170 additions and 139 deletions
  1. +6
    -13
      src/usage/decider-prove.md
  2. +0
    -1
      src/usage/decider-verify.md
  3. +42
    -8
      src/usage/fold.md
  4. +52
    -82
      src/usage/frontend-arkworks.md
  5. +49
    -27
      src/usage/frontend-circom.md
  6. +13
    -7
      src/usage/frontend.md
  7. +6
    -0
      src/usage/overview.md
  8. +2
    -1
      src/usage/solidity-verifier.md

+ 6
- 13
src/usage/decider-prove.md

@ -27,21 +27,14 @@ type DECIDER = 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).

+ 0
- 1
src/usage/decider-verify.md

@ -15,7 +15,6 @@ type DECIDER = Decider<
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,
)

+ 42
- 8
src/usage/fold.md

@ -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 @@ type NOVA = Nova<
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 @@ NOVA::verify(
)
.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).

+ 52
- 82
src/usage/frontend-arkworks.md

@ -4,7 +4,7 @@ Let's walk through different simple examples implementing the `FCircuit` trait.
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 @@ pub struct CubicFCircuit {
}
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 @@ impl FCircuit for CubicFCircuit {
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 @@ impl FCircuit for CubicFCircuit {
```
## 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 @@ The circuit we will fold has a state of 5 public elements. At each step, we will
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 @@ Let's implement this now:
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 @@ impl FCircuit for MultiInputsFCircuit {
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,66 +113,12 @@ impl FCircuit for MultiInputsFCircuit {
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
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.
@ -200,6 +151,15 @@ 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}$.
```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 @@ where
_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 @@ where
/// 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 @@ where
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 @@ where
}
}
```
<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).

+ 49
- 27
src/usage/frontend-circom.md

@ -8,25 +8,33 @@ We can define the circuit to be folded in Circom. The only interface that we nee
```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 @@ template CubicCircuit() {
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).

+ 13
- 7
src/usage/frontend.md

@ -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 @@ To be folded with sonobe, a circuit needs to implement the [`FCircuit` trait](ht
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 @@ pub trait FCircuit: Clone + Debug {
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.

+ 6
- 0
src/usage/overview.md

@ -5,3 +5,9 @@ This section showcases how to use the Sonobe library to:
- 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>

+ 2
- 1
src/usage/solidity-verifier.md

@ -30,7 +30,7 @@ let calldata: Vec = prepare_calldata(
.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();

Loading…
Cancel
Save