// 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).
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).
@ -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> {
/// 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
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}$.
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).
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:
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");
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).
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:
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