Browse Source

add circom frontend example, in arkworks frontend replace the 'private' inputs example by the external inputs example

main
arnaucube 1 year ago
parent
commit
f736fa8ef8
4 changed files with 275 additions and 177 deletions
  1. +2
    -0
      src/SUMMARY.md
  2. +243
    -0
      src/usage/frontend-arkworks.md
  3. +30
    -0
      src/usage/frontend-circom.md
  4. +0
    -177
      src/usage/frontend.md

+ 2
- 0
src/SUMMARY.md

@ -5,6 +5,8 @@
- [Usage](usage/overview.md)
- [Frontend](usage/frontend.md)
- [Arkworks](usage/frontend-arkworks.md)
- [Circom](usage/frontend-circom.md)
- [Fold](usage/fold.md)
- [Decider prove](usage/decider-prove.md)
- [Decider verify](usage/decider-verify.md)

+ 243
- 0
src/usage/frontend-arkworks.md

@ -0,0 +1,243 @@
# Arkworks frontend
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
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.
```rust
#[derive(Clone, Copy, Debug)]
pub struct CubicFCircuit<F: PrimeField> {
_f: PhantomData<F>,
}
impl<F: PrimeField> FCircuit<F> for CubicFCircuit<F> {
type Params = ();
fn new(_params: Self::Params) -> Self {
Self { _f: PhantomData }
}
fn state_len(&self) -> usize {
1
}
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,
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))?;
let z_i = z_i[0].clone();
Ok(vec![&z_i * &z_i * &z_i + &z_i + &five])
}
}
```
## Folding a simple circuit
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:
1. adding 4 to the first element
2. adding 40 to the second element
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.
#[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 state_len(&self) -> usize {
5 // This circuit has 5 inputs
}
// 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>) -> 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`
}
/// Generates R1CS 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> {
// 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))?;
let a = z_i[0].clone() + four.clone();
let b = z_i[1].clone() + forty.clone();
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
}
/// 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 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>>,
) -> 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
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]$.
```rust
#[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()])
}
}
```

+ 30
- 0
src/usage/frontend-circom.md

@ -0,0 +1,30 @@
# Circom frontend
We can define the circuit to be folded in Circom. The only interface that we need to fit in is:
```c
template FCircuit() {
signal input ivc_input[1];
signal output ivc_output[1];
// [...]
}
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.
So for example, the following circuit does the traditional example at each step, which proves knowledge of $x$ such that $y==x^3 + x + 5$ for a known $y$:
```c
pragma circom 2.0.3;
template Example () {
signal input ivc_input[1];
signal output ivc_output[1];
signal temp;
temp <== ivc_input[0] * ivc_input[0];
ivc_output[0] <== temp * ivc_input[0] + ivc_input[0] + 5;
}
component main {public [ivc_input]} = Example();
```

+ 0
- 177
src/usage/frontend.md

@ -43,180 +43,3 @@ pub trait FCircuit: Clone + Debug {
}
```
# Example
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.
## Folding a simple circuit
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:
1. adding 4 to the first element
2. adding 40 to the second element
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.
#[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 state_len(&self) -> usize {
5 // This circuit has 5 inputs
}
// 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>) -> 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`
}
/// Generates R1CS 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> {
// 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))?;
let a = z_i[0].clone() + four.clone();
let b = z_i[1].clone() + forty.clone();
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.
```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
}
/// 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 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>>,
) -> 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()])
}
}
```
## Folding a circuit with public and private inputs
Sometimes, the circuit to be folded will have private inputs. Let's see how we can setup such a circuit to be folded with sonobe. Again, the logic here is also very similar to our previous examples. The main difference is that the `struct` which will hold the circuit also holds a `Vec` of private inputs.
```rust
#[derive(Clone, Debug)]
pub struct ACircuitWithPrivateState<F: PrimeField> {
_f: PhantomData<F>,
private_state: Vec<F>, // private inputs, here a `Vec` of field elements, but you can specify whatever type you prefer here
}
impl<F: PrimeField> FCircuit<F> for ACircuitWithPrivateState<F> {
type Params = Vec<F>;
fn new(params: Self::Params) -> Self {
Self {
_f: PhantomData,
private_state: params,
}
}
fn state_len(&self) -> usize {
3 // the length of the state should match the size of the public inputs, not including the private inputs
}
fn step_native(&self, i: usize, z_i: Vec<F>) -> Result<Vec<F>, folding_schemes::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 = self.private_state[0] * a;
let e = self.private_state[1] * c;
Ok(vec![a, b, c])
Ok(new_z_i)
}
fn generate_step_constraints(
&self,
cs: ConstraintSystemRef<F>,
i: usize,
z_i: Vec<ark_r1cs_std::fields::fp::FpVar<F>>,
) -> Result<Vec<ark_r1cs_std::fields::fp::FpVar<F>>, SynthesisError> {
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))?;
// we still need to allocate our private state as witnesses
let priv_var_0 = FpVar::<F>::new_witness(cs.clone(), || Ok(self.private_state[0].clone()))?;
let priv_var_1 = FpVar::<F>::new_witness(cs.clone(), || Ok(self.private_state[1].clone()))?;
let a = z_i[0].clone() + four.clone();
let b = z_i[1].clone() + forty.clone();
let c = z_i[2].clone() * four;
let d = priv_var_0 * a;
let e = priv_var_1 * c;
Ok(vec![a, b, c])
}
}
```

Loading…
Cancel
Save