update to newer sonobe's interface

This commit is contained in:
arnaucube
2024-08-06 21:04:03 +02:00
parent 0a10de1e00
commit 2dadf599d2
8 changed files with 170 additions and 139 deletions

View File

@@ -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).

View File

@@ -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,
)

View File

@@ -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).

View File

@@ -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<F: PrimeField> {
}
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<F: PrimeField> FCircuit<F> for CubicFCircuit<F> {
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<F: PrimeField> FCircuit<F> for CubicFCircuit<F> {
```
## 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<F: PrimeField> FCircuit<F> for MultiInputsFCircuit<F> {
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,65 +113,11 @@ impl<F: PrimeField> FCircuit<F> for MultiInputsFCircuit<F> {
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`
Ok(vec![a, b, c, d, e])
}
}
```
## 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()])
}
}
```
## Using external inputs
@@ -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).

View File

@@ -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,27 +49,30 @@ 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 =
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 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<
pub type N =
Nova<G1, GVar, G2, GVar2, CircomFCircuit<Fr>, KZG<'static, Bn254>, Pedersen<G2>, false>;
pub type D = DeciderEth<
G1,
GVar,
G2,
@@ -70,18 +81,29 @@ pub type DECIDERETH_FCircuit = DeciderEth<
KZG<'static, Bn254>,
Pedersen<G2>,
Groth16<Bn254>,
NOVA,
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).

View File

@@ -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,10 +20,10 @@ 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;
@@ -25,7 +31,7 @@ pub trait FCircuit<F: PrimeField>: Clone + Debug {
/// 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<F: PrimeField>: 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.

View File

@@ -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>

View File

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