Move the experimental frontends into a separate crate, so that when not using them they don't take several minutes to compile (and indirect dependencies). (#168)

This saves several minutes (and MBs of data) on compilation time both
when running tests in this repo, but also when using the sonobe lib as a
dependency in external repos.
This commit is contained in:
2024-10-19 18:49:40 +02:00
committed by GitHub
parent cb1b8e37aa
commit 234600b39f
34 changed files with 164 additions and 133 deletions

33
frontends/Cargo.toml Normal file
View File

@@ -0,0 +1,33 @@
[package]
name = "frontends"
version = "0.1.0"
edition = "2021"
[dependencies]
ark-ff = { version = "^0.4.0", default-features = false, features = ["parallel", "asm"] }
ark-std = { version = "^0.4.0", default-features = false, features = ["parallel"] }
ark-relations = { version = "^0.4.0", default-features = false }
# ark-r1cs-std is patched at the workspace level
ark-r1cs-std = { version = "0.4.0", default-features = false, features = ["parallel"] }
ark-serialize = { version = "^0.4.0", default-features = false }
ark-circom = { git = "https://github.com/arnaucube/circom-compat", default-features = false }
num-bigint = "0.4"
ark-noname = { git = "https://github.com/dmpierre/ark-noname", branch = "feat/sonobe-integration" }
noname = { git = "https://github.com/dmpierre/noname" }
serde_json = "1.0.85" # to (de)serialize JSON
acvm = { git = "https://github.com/noir-lang/noir", rev="2b4853e", default-features = false }
noir_arkworks_backend = { package="arkworks_backend", git = "https://github.com/dmpierre/arkworks_backend", branch = "feat/sonobe-integration" }
folding-schemes = { path = "../folding-schemes/"}
[dev-dependencies]
ark-bn254 = {version="0.4.0", features=["r1cs"]}
# This allows the crate to be built when targeting WASM.
# See more at: https://docs.rs/getrandom/#webassembly-support
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
[features]
default = ["ark-circom/default", "parallel"]
parallel = []
wasm = ["ark-circom/wasm"]

18
frontends/README.md Normal file
View File

@@ -0,0 +1,18 @@
# frontends
This crate contains *experimental frontends* for Sonobe.
The recommended frontend is to directly use [arkworks](https://github.com/arkworks-rs) to define the FCircuit, just following the [`FCircuit` trait](https://github.com/privacy-scaling-explorations/sonobe/blob/main/folding-schemes/src/frontend/mod.rs).
## Experimental frontends
> Warning: the following frontends are experimental and some computational and time overhead is expected when using them compared to directly using the [arkworks frontend](https://github.com/privacy-scaling-explorations/sonobe/blob/main/folding-schemes/src/frontend/mod.rs).
Available experimental frontends:
- [Circom](https://github.com/iden3/circom), iden3, 0Kims Association. Supported version`<=v2.1.9`.
- [Noir](https://github.com/noir-lang/noir), Aztec.
- [Noname](https://github.com/zksecurity/noname), zkSecurity. Partially supported.
Documentation about frontend interface and experimental frontends: https://privacy-scaling-explorations.github.io/sonobe-docs/usage/frontend.html
## Implementing new frontends
Support for new frontends can be added (even from outside this repo) by implementing the [`FCircuit` trait](https://github.com/privacy-scaling-explorations/sonobe/blob/main/folding-schemes/src/frontend/mod.rs).

380
frontends/src/circom/mod.rs Normal file
View File

@@ -0,0 +1,380 @@
use ark_circom::circom::{CircomCircuit, R1CS as CircomR1CS};
use ark_ff::PrimeField;
use ark_r1cs_std::alloc::AllocVar;
use ark_r1cs_std::fields::fp::FpVar;
use ark_r1cs_std::fields::fp::FpVar::Var;
use ark_r1cs_std::R1CSVar;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
use ark_std::fmt::Debug;
use folding_schemes::{frontend::FCircuit, utils::PathOrBin, Error};
use num_bigint::BigInt;
use std::rc::Rc;
use std::{fmt, usize};
pub mod utils;
use utils::CircomWrapper;
type ClosurePointer<F> = Rc<dyn Fn(usize, Vec<F>, Vec<F>) -> Result<Vec<F>, Error>>;
#[derive(Clone)]
struct CustomStepNative<F: PrimeField> {
func: ClosurePointer<F>,
}
impl<F: PrimeField> fmt::Debug for CustomStepNative<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Function pointer: {:?}",
std::any::type_name::<fn(usize, Vec<F>, Vec<F>) -> Result<Vec<F>, Error>>()
)
}
}
/// Define CircomFCircuit
#[derive(Clone, Debug)]
pub struct CircomFCircuit<F: PrimeField> {
circom_wrapper: CircomWrapper<F>,
pub state_len: usize,
pub external_inputs_len: usize,
r1cs: CircomR1CS<F>,
custom_step_native_code: Option<CustomStepNative<F>>,
}
impl<F: PrimeField> CircomFCircuit<F> {
pub fn set_custom_step_native(&mut self, func: ClosurePointer<F>) {
self.custom_step_native_code = Some(CustomStepNative::<F> { func });
}
pub fn execute_custom_step_native(
&self,
_i: usize,
z_i: Vec<F>,
external_inputs: Vec<F>,
) -> Result<Vec<F>, Error> {
if let Some(code) = &self.custom_step_native_code {
(code.func)(_i, z_i, external_inputs)
} else {
#[cfg(test)]
assert_eq!(z_i.len(), self.state_len());
#[cfg(test)]
assert_eq!(external_inputs.len(), self.external_inputs_len());
let inputs_bi = z_i
.iter()
.map(|val| self.circom_wrapper.ark_primefield_to_num_bigint(*val))
.collect::<Vec<BigInt>>();
let mut inputs_map = vec![("ivc_input".to_string(), inputs_bi)];
if self.external_inputs_len() > 0 {
let external_inputs_bi = external_inputs
.iter()
.map(|val| self.circom_wrapper.ark_primefield_to_num_bigint(*val))
.collect::<Vec<BigInt>>();
inputs_map.push(("external_inputs".to_string(), external_inputs_bi));
}
// Computes witness
let witness = self
.circom_wrapper
.extract_witness(&inputs_map)
.map_err(|e| {
Error::WitnessCalculationError(format!("Failed to calculate witness: {}", e))
})?;
// Extracts the z_i1(next state) from the witness vector.
let z_i1 = witness[1..1 + self.state_len()].to_vec();
Ok(z_i1)
}
}
}
impl<F: PrimeField> FCircuit<F> for CircomFCircuit<F> {
/// (r1cs_path, wasm_path, state_len, external_inputs_len)
type Params = (PathOrBin, PathOrBin, usize, usize);
fn new(params: Self::Params) -> Result<Self, Error> {
let (r1cs_path, wasm_path, state_len, external_inputs_len) = params;
let circom_wrapper = CircomWrapper::new(r1cs_path, wasm_path)?;
let r1cs = circom_wrapper.extract_r1cs()?;
Ok(Self {
circom_wrapper,
state_len,
external_inputs_len,
r1cs,
custom_step_native_code: None,
})
}
fn state_len(&self) -> usize {
self.state_len
}
fn external_inputs_len(&self) -> usize {
self.external_inputs_len
}
fn step_native(
&self,
_i: usize,
z_i: Vec<F>,
external_inputs: Vec<F>,
) -> Result<Vec<F>, Error> {
self.execute_custom_step_native(_i, z_i, external_inputs)
}
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> {
#[cfg(test)]
assert_eq!(z_i.len(), self.state_len());
#[cfg(test)]
assert_eq!(external_inputs.len(), self.external_inputs_len());
let input_values = self.fpvars_to_bigints(&z_i)?;
let mut inputs_map = vec![("ivc_input".to_string(), input_values)];
if self.external_inputs_len() > 0 {
let external_inputs_bi = self.fpvars_to_bigints(&external_inputs)?;
inputs_map.push(("external_inputs".to_string(), external_inputs_bi));
}
let witness = self
.circom_wrapper
.extract_witness(&inputs_map)
.map_err(|_| SynthesisError::AssignmentMissing)?;
// Since public inputs are already allocated variables, we will tell `circom-compat` to not re-allocate those
let mut already_allocated_public_inputs = vec![];
for var in z_i.iter() {
match var {
Var(var) => already_allocated_public_inputs.push(var.variable),
_ => return Err(SynthesisError::Unsatisfiable), // allocated z_i should be Var
}
}
// Initializes the CircomCircuit.
let circom_circuit = CircomCircuit {
r1cs: self.r1cs.clone(),
witness: Some(witness.clone()),
public_inputs_indexes: already_allocated_public_inputs,
allocate_inputs_as_witnesses: true,
};
// Generates the constraints for the circom_circuit.
circom_circuit.generate_constraints(cs.clone())?;
// TODO: https://github.com/privacy-scaling-explorations/sonobe/issues/104
// We disable checking constraints for now
// Checks for constraint satisfaction.
// if !cs.is_satisfied().unwrap() {
// return Err(SynthesisError::Unsatisfiable);
// }
// Extracts the z_i1(next state) from the witness vector.
let z_i1: Vec<FpVar<F>> = Vec::<FpVar<F>>::new_witness(cs.clone(), || {
Ok(witness[1..1 + self.state_len()].to_vec())
})?;
Ok(z_i1)
}
}
impl<F: PrimeField> CircomFCircuit<F> {
fn fpvars_to_bigints(&self, fpvars: &[FpVar<F>]) -> Result<Vec<BigInt>, SynthesisError> {
let mut input_values = Vec::new();
// converts each FpVar to PrimeField value, then to num_bigint::BigInt.
for fp_var in fpvars.iter() {
// extracts the PrimeField value from FpVar.
let primefield_value = fp_var.value()?;
// converts the PrimeField value to num_bigint::BigInt.
let num_bigint_value = self
.circom_wrapper
.ark_primefield_to_num_bigint(primefield_value);
input_values.push(num_bigint_value);
}
Ok(input_values)
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_bn254::Fr;
use ark_relations::r1cs::ConstraintSystem;
use std::path::PathBuf;
// Tests the step_native function of CircomFCircuit.
#[test]
fn test_circom_step_native() {
let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs");
let wasm_path =
PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
let circom_fcircuit =
CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
let z_i = vec![Fr::from(3u32)];
let z_i1 = circom_fcircuit.step_native(1, z_i, vec![]).unwrap();
assert_eq!(z_i1, vec![Fr::from(35u32)]);
}
// Tests the generate_step_constraints function of CircomFCircuit.
#[test]
fn test_circom_step_constraints() {
let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs");
let wasm_path =
PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
let circom_fcircuit =
CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
let cs = ConstraintSystem::<Fr>::new_ref();
let z_i = vec![Fr::from(3u32)];
let z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i)).unwrap();
let z_i1_var = circom_fcircuit
.generate_step_constraints(cs.clone(), 1, z_i_var, vec![])
.unwrap();
assert_eq!(z_i1_var.value().unwrap(), vec![Fr::from(35u32)]);
}
// Tests the WrapperCircuit with CircomFCircuit.
#[test]
fn test_wrapper_circomtofcircuit() {
let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs");
let wasm_path =
PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
let circom_fcircuit =
CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
// Allocates z_i1 by using step_native function.
let z_i = vec![Fr::from(3_u32)];
let wrapper_circuit = folding_schemes::frontend::utils::WrapperCircuit {
FC: circom_fcircuit.clone(),
z_i: Some(z_i.clone()),
z_i1: Some(circom_fcircuit.step_native(0, z_i.clone(), vec![]).unwrap()),
};
let cs = ConstraintSystem::<Fr>::new_ref();
wrapper_circuit.generate_constraints(cs.clone()).unwrap();
assert!(
cs.is_satisfied().unwrap(),
"Constraint system is not satisfied"
);
}
#[test]
fn test_circom_external_inputs() {
let r1cs_path = PathBuf::from("./src/circom/test_folder/with_external_inputs.r1cs");
let wasm_path = PathBuf::from(
"./src/circom/test_folder/with_external_inputs_js/with_external_inputs.wasm",
);
let circom_fcircuit =
CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 2)).unwrap(); // state_len:1, external_inputs_len:2
let cs = ConstraintSystem::<Fr>::new_ref();
let z_i = vec![Fr::from(3u32)];
let external_inputs = vec![Fr::from(6u32), Fr::from(7u32)];
// run native step
let z_i1_native = circom_fcircuit
.step_native(1, z_i.clone(), external_inputs.clone())
.unwrap();
// run gadget step
let z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i)).unwrap();
let external_inputs_var =
Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(external_inputs.clone())).unwrap();
let z_i1_var = circom_fcircuit
.generate_step_constraints(cs.clone(), 1, z_i_var, external_inputs_var)
.unwrap();
assert_eq!(z_i1_var.value().unwrap(), z_i1_native);
// re-init cs and run gadget step with wrong ivc inputs (first ivc should not be zero)
let cs = ConstraintSystem::<Fr>::new_ref();
let wrong_z_i = vec![Fr::from(0)];
let wrong_z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(wrong_z_i)).unwrap();
let external_inputs_var =
Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(external_inputs)).unwrap();
let _z_i1_var = circom_fcircuit.generate_step_constraints(
cs.clone(),
1,
wrong_z_i_var,
external_inputs_var,
);
// TODO:: https://github.com/privacy-scaling-explorations/sonobe/issues/104
// Disable check for now
// assert!(z_i1_var.is_err());
}
#[test]
fn test_circom_no_external_inputs() {
let r1cs_path = PathBuf::from("./src/circom/test_folder/no_external_inputs.r1cs");
let wasm_path =
PathBuf::from("./src/circom/test_folder/no_external_inputs_js/no_external_inputs.wasm");
let circom_fcircuit =
CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 3, 0)).unwrap();
let cs = ConstraintSystem::<Fr>::new_ref();
let z_i = vec![Fr::from(3u32), Fr::from(4u32), Fr::from(5u32)];
let z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i.clone())).unwrap();
// run native step
let z_i1_native = circom_fcircuit.step_native(1, z_i.clone(), vec![]).unwrap();
// run gadget step
let z_i1_var = circom_fcircuit
.generate_step_constraints(cs.clone(), 1, z_i_var, vec![])
.unwrap();
assert_eq!(z_i1_var.value().unwrap(), z_i1_native);
// re-init cs and run gadget step with wrong ivc inputs (first ivc input should not be zero)
let cs = ConstraintSystem::<Fr>::new_ref();
let wrong_z_i = vec![Fr::from(0u32), Fr::from(4u32), Fr::from(5u32)];
let wrong_z_i_var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(wrong_z_i)).unwrap();
let _z_i1_var =
circom_fcircuit.generate_step_constraints(cs.clone(), 1, wrong_z_i_var, vec![]);
// TODO:: https://github.com/privacy-scaling-explorations/sonobe/issues/104
// Disable check for now
// assert!(z_i1_var.is_err())
}
#[test]
fn test_custom_code() {
let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs");
let wasm_path =
PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
let mut circom_fcircuit =
CircomFCircuit::<Fr>::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0
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)])
}));
// Allocates z_i1 by using step_native function.
let z_i = vec![Fr::from(3_u32)];
let wrapper_circuit = folding_schemes::frontend::utils::WrapperCircuit {
FC: circom_fcircuit.clone(),
z_i: Some(z_i.clone()),
z_i1: Some(circom_fcircuit.step_native(0, z_i.clone(), vec![]).unwrap()),
};
let cs = ConstraintSystem::<Fr>::new_ref();
wrapper_circuit.generate_constraints(cs.clone()).unwrap();
assert!(
cs.is_satisfied().unwrap(),
"Constraint system is not satisfied"
);
}
}

View File

@@ -0,0 +1,14 @@
pragma circom 2.0.0;
// From: https://github.com/iden3/circomlib/blob/master/circuits/comparators.circom
template IsZero() {
signal input in;
signal output out;
signal inv;
inv <-- in!=0 ? 1/in : 0;
out <== -in*inv +1;
in*out === 0;
}

View File

@@ -0,0 +1,4 @@
#!/bin/bash
circom ./frontends/src/circom/test_folder/cubic_circuit.circom --r1cs --sym --wasm --prime bn128 --output ./frontends/src/circom/test_folder/
circom ./frontends/src/circom/test_folder/with_external_inputs.circom --r1cs --sym --wasm --prime bn128 --output ./frontends/src/circom/test_folder/
circom ./frontends/src/circom/test_folder/no_external_inputs.circom --r1cs --sym --wasm --prime bn128 --output ./frontends/src/circom/test_folder/

View File

@@ -0,0 +1,12 @@
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();

View File

@@ -0,0 +1,23 @@
pragma circom 2.0.3;
include "./circuits/is_zero.circom";
template NoExternalInputs () {
signal input ivc_input[3];
signal output ivc_output[3];
component check_input = IsZero();
check_input.in <== ivc_input[0];
check_input.out === 0;
signal temp1;
signal temp2;
temp1 <== ivc_input[0] * ivc_input[1];
temp2 <== temp1 * ivc_input[2];
ivc_output[0] <== temp1 * ivc_input[0];
ivc_output[1] <== temp1 * ivc_input[1] + temp1;
ivc_output[2] <== temp1 * ivc_input[2] + temp2;
}
component main {public [ivc_input]} = NoExternalInputs();

View File

@@ -0,0 +1,22 @@
pragma circom 2.0.3;
include "./circuits/is_zero.circom";
template WithExternalInputs () {
signal input ivc_input[1];
signal input external_inputs[2];
signal output ivc_output[1];
component check_input = IsZero();
check_input.in <== ivc_input[0];
check_input.out === 0;
signal temp1;
signal temp2;
temp1 <== ivc_input[0] * ivc_input[0];
temp2 <== ivc_input[0] * external_inputs[0];
ivc_output[0] <== temp1 * ivc_input[0] + temp2 + external_inputs[1];
}
component main {public [ivc_input]} = WithExternalInputs();

View File

@@ -0,0 +1,179 @@
use ark_circom::{
circom::{r1cs_reader, R1CS},
WitnessCalculator,
};
use ark_ff::{BigInteger, PrimeField};
use ark_serialize::Read;
use num_bigint::{BigInt, Sign};
use std::{fs::File, io::Cursor, marker::PhantomData, path::PathBuf};
use folding_schemes::{utils::PathOrBin, Error};
// A struct that wraps Circom functionalities, allowing for extraction of R1CS and witnesses
// based on file paths to Circom's .r1cs and .wasm.
#[derive(Clone, Debug)]
pub struct CircomWrapper<F: PrimeField> {
r1csfile_bytes: Vec<u8>,
wasmfile_bytes: Vec<u8>,
_marker: PhantomData<F>,
}
impl<F: PrimeField> CircomWrapper<F> {
// Creates a new instance of the CircomWrapper with the file paths.
pub fn new(r1cs: PathOrBin, wasm: PathOrBin) -> Result<Self, Error> {
match (r1cs, wasm) {
(PathOrBin::Path(r1cs_path), PathOrBin::Path(wasm_path)) => {
Self::new_from_path(r1cs_path, wasm_path)
}
(PathOrBin::Bin(r1cs_bin), PathOrBin::Bin(wasm_bin)) => Ok(Self {
r1csfile_bytes: r1cs_bin,
wasmfile_bytes: wasm_bin,
_marker: PhantomData,
}),
_ => unreachable!("You should pass the same enum branch for both inputs"),
}
}
// Creates a new instance of the CircomWrapper with the file paths.
fn new_from_path(r1cs_file_path: PathBuf, wasm_file_path: PathBuf) -> Result<Self, Error> {
let mut file = File::open(r1cs_file_path)?;
let metadata = File::metadata(&file)?;
let mut r1csfile_bytes = vec![0; metadata.len() as usize];
file.read_exact(&mut r1csfile_bytes)?;
let mut file = File::open(wasm_file_path)?;
let metadata = File::metadata(&file)?;
let mut wasmfile_bytes = vec![0; metadata.len() as usize];
file.read_exact(&mut wasmfile_bytes)?;
Ok(CircomWrapper {
r1csfile_bytes,
wasmfile_bytes,
_marker: PhantomData,
})
}
// Aggregated function to obtain R1CS and witness from Circom.
pub fn extract_r1cs_and_witness(
&self,
inputs: &[(String, Vec<BigInt>)],
) -> Result<(R1CS<F>, Option<Vec<F>>), Error> {
// Extracts the R1CS
let r1cs_file = r1cs_reader::R1CSFile::<F>::new(Cursor::new(&self.r1csfile_bytes))?;
let r1cs = r1cs_reader::R1CS::<F>::from(r1cs_file);
// Extracts the witness vector
let witness_vec = self.extract_witness(inputs)?;
Ok((r1cs, Some(witness_vec)))
}
pub fn extract_r1cs(&self) -> Result<R1CS<F>, Error> {
let r1cs_file = r1cs_reader::R1CSFile::<F>::new(Cursor::new(&self.r1csfile_bytes))?;
let mut r1cs = r1cs_reader::R1CS::<F>::from(r1cs_file);
r1cs.wire_mapping = None;
Ok(r1cs)
}
// Extracts the witness vector as a vector of PrimeField elements.
pub fn extract_witness(&self, inputs: &[(String, Vec<BigInt>)]) -> Result<Vec<F>, Error> {
let witness_bigint = self.calculate_witness(inputs)?;
witness_bigint
.iter()
.map(|big_int| {
self.num_bigint_to_ark_bigint(big_int)
.and_then(|ark_big_int| {
F::from_bigint(ark_big_int)
.ok_or_else(|| Error::Other("could not get F from bigint".to_string()))
})
})
.collect()
}
// Calculates the witness given the Wasm filepath and inputs.
pub fn calculate_witness(
&self,
inputs: &[(String, Vec<BigInt>)],
) -> Result<Vec<BigInt>, Error> {
let mut calculator = WitnessCalculator::from_binary(&self.wasmfile_bytes).map_err(|e| {
Error::WitnessCalculationError(format!("Failed to create WitnessCalculator: {}", e))
})?;
calculator
.calculate_witness(inputs.iter().cloned(), true)
.map_err(|e| {
Error::WitnessCalculationError(format!("Failed to calculate witness: {}", e))
})
}
// Converts a num_bigint::BigInt to a PrimeField::BigInt.
pub fn num_bigint_to_ark_bigint(&self, value: &BigInt) -> Result<F::BigInt, Error> {
let big_uint = value
.to_biguint()
.ok_or_else(|| Error::BigIntConversionError("BigInt is negative".to_string()))?;
F::BigInt::try_from(big_uint).map_err(|_| {
Error::BigIntConversionError("Failed to convert to PrimeField::BigInt".to_string())
})
}
// Converts a PrimeField element to a num_bigint::BigInt representation.
pub fn ark_primefield_to_num_bigint(&self, value: F) -> BigInt {
let primefield_bigint: F::BigInt = value.into_bigint();
let bytes = primefield_bigint.to_bytes_be();
BigInt::from_bytes_be(Sign::Plus, &bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ark_bn254::Fr;
use ark_circom::circom::{CircomBuilder, CircomConfig};
use ark_circom::CircomCircuit;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem};
//To generate .r1cs and .wasm files, run the below command in the terminal.
//bash ./frontends/src/circom/test_folder/compile.sh
// Test the satisfication by using the CircomBuilder of circom-compat
#[test]
fn test_circombuilder_satisfied() {
let cfg = CircomConfig::<Fr>::new(
"./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm",
"./src/circom/test_folder/cubic_circuit.r1cs",
)
.unwrap();
let mut builder = CircomBuilder::new(cfg);
builder.push_input("ivc_input", 3);
let circom = builder.build().unwrap();
let cs = ConstraintSystem::<Fr>::new_ref();
circom.generate_constraints(cs.clone()).unwrap();
assert!(cs.is_satisfied().unwrap());
}
// Test the satisfication by using the CircomWrapper
#[test]
fn test_extract_r1cs_and_witness() {
let r1cs_path = PathBuf::from("./src/circom/test_folder/cubic_circuit.r1cs");
let wasm_path =
PathBuf::from("./src/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");
let inputs = vec![("ivc_input".to_string(), vec![BigInt::from(3)])];
let wrapper = CircomWrapper::<Fr>::new(r1cs_path.into(), wasm_path.into()).unwrap();
let (r1cs, witness) = wrapper.extract_r1cs_and_witness(&inputs).unwrap();
let cs = ConstraintSystem::<Fr>::new_ref();
let circom_circuit = CircomCircuit {
r1cs,
witness,
public_inputs_indexes: vec![],
allocate_inputs_as_witnesses: false,
};
circom_circuit.generate_constraints(cs.clone()).unwrap();
assert!(cs.is_satisfied().unwrap());
}
}

3
frontends/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod circom;
pub mod noir;
pub mod noname;

303
frontends/src/noir/mod.rs Normal file
View File

@@ -0,0 +1,303 @@
use std::collections::HashMap;
use acvm::{
acir::{
acir_field::GenericFieldElement,
circuit::{Circuit, Program},
native_types::{Witness as AcvmWitness, WitnessMap},
},
blackbox_solver::StubbedBlackBoxSolver,
pwg::ACVM,
};
use ark_ff::PrimeField;
use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, R1CSVar};
use ark_relations::r1cs::ConstraintSynthesizer;
use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError};
use folding_schemes::{frontend::FCircuit, utils::PathOrBin, Error};
use noir_arkworks_backend::{
read_program_from_binary, read_program_from_file, sonobe_bridge::AcirCircuitSonobe,
};
#[derive(Clone, Debug)]
pub struct NoirFCircuit<F: PrimeField> {
pub circuit: Circuit<GenericFieldElement<F>>,
pub state_len: usize,
pub external_inputs_len: usize,
}
impl<F: PrimeField> FCircuit<F> for NoirFCircuit<F> {
type Params = (PathOrBin, usize, usize);
fn new(params: Self::Params) -> Result<Self, Error> {
let (source, state_len, external_inputs_len) = params;
let program = match source {
PathOrBin::Path(path) => read_program_from_file(path),
PathOrBin::Bin(bytes) => read_program_from_binary(&bytes),
}
.map_err(|ee| Error::Other(format!("{:?}", ee)))?;
let circuit: Circuit<GenericFieldElement<F>> = program.functions[0].clone();
let ivc_input_length = circuit.public_parameters.0.len();
let ivc_return_length = circuit.return_values.0.len();
if ivc_input_length != ivc_return_length {
return Err(Error::NotSameLength(
"IVC input: ".to_string(),
ivc_input_length,
"IVC output: ".to_string(),
ivc_return_length,
));
}
Ok(NoirFCircuit {
circuit,
state_len,
external_inputs_len,
})
}
fn state_len(&self) -> usize {
self.state_len
}
fn external_inputs_len(&self) -> usize {
self.external_inputs_len
}
fn step_native(
&self,
_i: usize,
z_i: Vec<F>,
external_inputs: Vec<F>, // inputs that are not part of the state
) -> Result<Vec<F>, Error> {
let mut acvm = ACVM::new(
&StubbedBlackBoxSolver,
&self.circuit.opcodes,
WitnessMap::new(),
&[],
&[],
);
self.circuit
.public_parameters
.0
.iter()
.map(|witness| {
let idx: usize = witness.as_usize();
let value = z_i[idx].to_string();
let witness = AcvmWitness(witness.witness_index());
let f = GenericFieldElement::<F>::try_from_str(&value)
.ok_or(SynthesisError::Unsatisfiable)?;
acvm.overwrite_witness(witness, f);
Ok(())
})
.collect::<Result<Vec<()>, SynthesisError>>()?;
// write witness values for external_inputs
self.circuit
.private_parameters
.iter()
.map(|witness| {
let idx = witness.as_usize() - z_i.len();
let value = external_inputs[idx].to_string();
let f = GenericFieldElement::<F>::try_from_str(&value)
.ok_or(SynthesisError::Unsatisfiable)?;
acvm.overwrite_witness(AcvmWitness(witness.witness_index()), f);
Ok(())
})
.collect::<Result<Vec<()>, SynthesisError>>()?;
let _ = acvm.solve();
let witness_map = acvm.finalize();
// get the z_{i+1} output state
let assigned_z_i1 = self
.circuit
.return_values
.0
.iter()
.map(|witness| {
let noir_field_element = witness_map
.get(witness)
.ok_or(SynthesisError::AssignmentMissing)?;
Ok(noir_field_element.into_repr())
})
.collect::<Result<Vec<F>, SynthesisError>>()?;
Ok(assigned_z_i1)
}
fn generate_step_constraints(
&self,
cs: ConstraintSystemRef<F>,
_i: usize,
z_i: Vec<FpVar<F>>,
external_inputs: Vec<FpVar<F>>, // inputs that are not part of the state
) -> Result<Vec<FpVar<F>>, SynthesisError> {
let mut acvm = ACVM::new(
&StubbedBlackBoxSolver,
&self.circuit.opcodes,
WitnessMap::new(),
&[],
&[],
);
let mut already_assigned_witness_values = HashMap::new();
self.circuit
.public_parameters
.0
.iter()
.map(|witness| {
let idx: usize = witness.as_usize();
let witness = AcvmWitness(witness.witness_index());
already_assigned_witness_values.insert(witness, &z_i[idx]);
let val = z_i[idx].value()?;
let value = if val == F::zero() {
"0".to_string()
} else {
val.to_string()
};
let f = GenericFieldElement::<F>::try_from_str(&value)
.ok_or(SynthesisError::Unsatisfiable)?;
acvm.overwrite_witness(witness, f);
Ok(())
})
.collect::<Result<Vec<()>, SynthesisError>>()?;
// write witness values for external_inputs
self.circuit
.private_parameters
.iter()
.map(|witness| {
let idx = witness.as_usize() - z_i.len();
let witness = AcvmWitness(witness.witness_index());
already_assigned_witness_values.insert(witness, &external_inputs[idx]);
let val = external_inputs[idx].value()?;
let value = if val == F::zero() {
"0".to_string()
} else {
val.to_string()
};
let f = GenericFieldElement::<F>::try_from_str(&value)
.ok_or(SynthesisError::Unsatisfiable)?;
acvm.overwrite_witness(witness, f);
Ok(())
})
.collect::<Result<Vec<()>, SynthesisError>>()?;
// computes the witness
let _ = acvm.solve();
let witness_map = acvm.finalize();
// get the z_{i+1} output state
let assigned_z_i1 = self
.circuit
.return_values
.0
.iter()
.map(|witness| {
let noir_field_element = witness_map
.get(witness)
.ok_or(SynthesisError::AssignmentMissing)?;
FpVar::<F>::new_witness(cs.clone(), || Ok(noir_field_element.into_repr()))
})
.collect::<Result<Vec<FpVar<F>>, SynthesisError>>()?;
// initialize circuit and set already assigned values
let mut acir_circuit = AcirCircuitSonobe::from((&self.circuit, witness_map));
acir_circuit.already_assigned_witnesses = already_assigned_witness_values;
acir_circuit.generate_constraints(cs.clone())?;
Ok(assigned_z_i1)
}
}
pub fn load_noir_circuit<F: PrimeField>(path: String) -> Circuit<GenericFieldElement<F>> {
let program: Program<GenericFieldElement<F>> = read_program_from_file(path).unwrap();
let circuit: Circuit<GenericFieldElement<F>> = program.functions[0].clone();
circuit
}
#[cfg(test)]
mod tests {
use crate::noir::load_noir_circuit;
use ark_bn254::Fr;
use ark_r1cs_std::R1CSVar;
use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar};
use ark_relations::r1cs::ConstraintSystem;
use folding_schemes::frontend::FCircuit;
use std::env;
use crate::noir::NoirFCircuit;
#[test]
fn test_step_native() {
let cur_path = env::current_dir().unwrap();
let circuit_path = format!(
"{}/src/noir/test_folder/test_circuit/target/test_circuit.json",
cur_path.to_str().unwrap()
);
let circuit = load_noir_circuit(circuit_path);
let noirfcircuit = NoirFCircuit {
circuit,
state_len: 2,
external_inputs_len: 2,
};
let inputs = vec![Fr::from(2), Fr::from(5)];
let res = noirfcircuit.step_native(0, inputs.clone(), inputs);
assert!(res.is_ok());
assert_eq!(res.unwrap(), vec![Fr::from(4), Fr::from(25)]);
}
#[test]
fn test_step_constraints() {
let cs = ConstraintSystem::<Fr>::new_ref();
let cur_path = env::current_dir().unwrap();
let circuit_path = format!(
"{}/src/noir/test_folder/test_circuit/target/test_circuit.json",
cur_path.to_str().unwrap()
);
let circuit = load_noir_circuit(circuit_path);
let noirfcircuit = NoirFCircuit {
circuit,
state_len: 2,
external_inputs_len: 2,
};
let inputs = vec![Fr::from(2), Fr::from(5)];
let z_i = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs.clone())).unwrap();
let external_inputs = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs)).unwrap();
let output = noirfcircuit
.generate_step_constraints(cs.clone(), 0, z_i, external_inputs)
.unwrap();
assert_eq!(output[0].value().unwrap(), Fr::from(4));
assert_eq!(output[1].value().unwrap(), Fr::from(25));
}
#[test]
fn test_step_constraints_no_external_inputs() {
let cs = ConstraintSystem::<Fr>::new_ref();
let cur_path = env::current_dir().unwrap();
let circuit_path = format!(
"{}/src/noir/test_folder/test_no_external_inputs/target/test_no_external_inputs.json",
cur_path.to_str().unwrap()
);
let circuit = load_noir_circuit(circuit_path);
let noirfcircuit = NoirFCircuit {
circuit,
state_len: 2,
external_inputs_len: 0,
};
let inputs = vec![Fr::from(2), Fr::from(5)];
let z_i = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs.clone())).unwrap();
let external_inputs = vec![];
let output = noirfcircuit
.generate_step_constraints(cs.clone(), 0, z_i, external_inputs)
.unwrap();
assert_eq!(output[0].value().unwrap(), Fr::from(4));
assert_eq!(output[1].value().unwrap(), Fr::from(25));
}
}

View File

@@ -0,0 +1,7 @@
#!/bin/bash
CUR_DIR=$(pwd)
TEST_PATH="${CUR_DIR}/frontends/src/noir/test_folder/"
for test_path in test_circuit test_mimc test_no_external_inputs; do
FOLDER="${TEST_PATH}${test_path}/"
cd ${FOLDER} && nargo compile && cd ${TEST_PATH}
done

View File

@@ -0,0 +1,8 @@
[package]
name = "test_circuit"
type = "bin"
authors = [""]
compiler_version = ">=0.30.0"
[dependencies]

View File

@@ -0,0 +1,11 @@
fn main(public_inputs: pub [Field; 2], private_inputs: [Field; 2]) -> pub [Field; 2]{
let a_pub = public_inputs[0];
let b_pub = public_inputs[1];
let c_private = private_inputs[0];
let d_private = private_inputs[1];
let out_1 = a_pub * c_private;
let out_2 = b_pub * d_private;
[out_1, out_2]
}

View File

@@ -0,0 +1,8 @@
[package]
name = "test_mimc"
type = "bin"
authors = [""]
compiler_version = ">=0.30.0"
[dependencies]

View File

@@ -0,0 +1,6 @@
use dep::std;
pub fn main(x: pub [Field; 1]) -> pub Field {
let hash = std::hash::mimc::mimc_bn254(x);
hash
}

View File

@@ -0,0 +1,8 @@
[package]
name = "test_no_external_inputs"
type = "bin"
authors = [""]
compiler_version = ">=0.30.0"
[dependencies]

View File

@@ -0,0 +1,9 @@
fn main(public_inputs: pub [Field; 2]) -> pub [Field; 2]{
let a_pub = public_inputs[0];
let b_pub = public_inputs[1];
let out_1 = a_pub * a_pub;
let out_2 = b_pub * b_pub;
[out_1, out_2]
}

200
frontends/src/noname/mod.rs Normal file
View File

@@ -0,0 +1,200 @@
use ark_noname::sonobe::NonameSonobeCircuit;
use ark_r1cs_std::alloc::AllocVar;
use ark_r1cs_std::fields::fp::FpVar;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
use num_bigint::BigUint;
use std::marker::PhantomData;
use self::utils::NonameInputs;
use ark_ff::PrimeField;
use ark_noname::utils::compile_source_code;
use folding_schemes::{frontend::FCircuit, Error};
use noname::backends::{r1cs::R1CS as R1CSNoname, BackendField};
use noname::witness::CompiledCircuit;
pub mod utils;
#[derive(Debug, Clone)]
pub struct NonameFCircuit<F: PrimeField, BF: BackendField> {
pub state_len: usize,
pub external_inputs_len: usize,
pub circuit: CompiledCircuit<R1CSNoname<BF>>,
_f: PhantomData<F>,
}
impl<F: PrimeField, BF: BackendField> FCircuit<F> for NonameFCircuit<F, BF> {
type Params = (String, usize, usize);
fn new(params: Self::Params) -> Result<Self, Error> {
let (code, state_len, external_inputs_len) = params;
let compiled_circuit = compile_source_code::<BF>(&code).map_err(|_| {
Error::Other("Encountered an error while compiling a noname circuit".to_owned())
})?;
Ok(NonameFCircuit {
state_len,
external_inputs_len,
circuit: compiled_circuit,
_f: PhantomData,
})
}
fn state_len(&self) -> usize {
self.state_len
}
fn external_inputs_len(&self) -> usize {
self.external_inputs_len
}
fn step_native(
&self,
_i: usize,
z_i: Vec<F>,
external_inputs: Vec<F>,
) -> Result<Vec<F>, Error> {
let wtns_external_inputs =
NonameInputs::from((&external_inputs, "external_inputs".to_string()));
let wtns_ivc_inputs = NonameInputs::from((&z_i, "ivc_inputs".to_string()));
let noname_witness = self
.circuit
.generate_witness(wtns_ivc_inputs.0, wtns_external_inputs.0)
.map_err(|e| Error::WitnessCalculationError(e.to_string()))?;
let z_i1_end_index = z_i.len() + 1;
let assigned_z_i1 = (1..z_i1_end_index)
.map(|idx| {
let value: BigUint = Into::into(noname_witness.witness[idx]);
F::from(value)
})
.collect();
Ok(assigned_z_i1)
}
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 wtns_external_inputs =
NonameInputs::from_fpvars((&external_inputs, "external_inputs".to_string()))?;
let wtns_ivc_inputs = NonameInputs::from_fpvars((&z_i, "ivc_inputs".to_string()))?;
let noname_witness = self
.circuit
.generate_witness(wtns_ivc_inputs.0, wtns_external_inputs.0)
.map_err(|_| SynthesisError::Unsatisfiable)?;
let z_i1_end_index = z_i.len() + 1;
let assigned_z_i1: Vec<FpVar<F>> = (1..z_i1_end_index)
.map(|idx| -> Result<FpVar<F>, SynthesisError> {
// the assigned zi1 is of the same size than the initial zi and is located in the
// output of the witness vector
// we prefer to assign z_i1 here since (1) we have to return it, (2) we cant return
// anything with the `generate_constraints` method used below
let value: BigUint = Into::into(noname_witness.witness[idx]);
let field_element = F::from(value);
FpVar::<F>::new_witness(cs.clone(), || Ok(field_element))
})
.collect::<Result<Vec<FpVar<F>>, SynthesisError>>()?;
let noname_circuit = NonameSonobeCircuit {
compiled_circuit: self.circuit.clone(),
witness: noname_witness,
assigned_z_i: &z_i,
assigned_external_inputs: &external_inputs,
assigned_z_i1: &assigned_z_i1,
};
noname_circuit.generate_constraints(cs.clone())?;
Ok(assigned_z_i1)
}
}
#[cfg(test)]
mod tests {
use ark_bn254::Fr;
use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, R1CSVar};
use noname::backends::r1cs::R1csBn254Field;
use folding_schemes::frontend::FCircuit;
use super::NonameFCircuit;
use ark_relations::r1cs::ConstraintSystem;
const NONAME_CIRCUIT_EXTERNAL_INPUTS: &str =
"fn main(pub ivc_inputs: [Field; 2], external_inputs: [Field; 2]) -> [Field; 2] {
let xx = external_inputs[0] + ivc_inputs[0];
let yy = external_inputs[1] * ivc_inputs[1];
assert_eq(yy, xx);
return [xx, yy];
}";
const NONAME_CIRCUIT_NO_EXTERNAL_INPUTS: &str =
"fn main(pub ivc_inputs: [Field; 2]) -> [Field; 2] {
let out = ivc_inputs[0] * ivc_inputs[1];
return [out, ivc_inputs[1]];
}";
#[test]
fn test_step_native() {
let cs = ConstraintSystem::<Fr>::new_ref();
let params = (NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(), 2, 2);
let circuit = NonameFCircuit::<Fr, R1csBn254Field>::new(params).unwrap();
let inputs_public = vec![Fr::from(2), Fr::from(5)];
let inputs_private = vec![Fr::from(8), Fr::from(2)];
let ivc_inputs_var =
Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs_public.clone())).unwrap();
let external_inputs_var =
Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs_private.clone())).unwrap();
let z_i1 = circuit
.generate_step_constraints(cs.clone(), 0, ivc_inputs_var, external_inputs_var)
.unwrap();
let z_i1_native = circuit
.step_native(0, inputs_public, inputs_private)
.unwrap();
assert_eq!(z_i1[0].value().unwrap(), z_i1_native[0]);
assert_eq!(z_i1[1].value().unwrap(), z_i1_native[1]);
}
#[test]
fn test_step_constraints() {
let cs = ConstraintSystem::<Fr>::new_ref();
let params = (NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(), 2, 2);
let circuit = NonameFCircuit::<Fr, R1csBn254Field>::new(params).unwrap();
let inputs_public = vec![Fr::from(2), Fr::from(5)];
let inputs_private = vec![Fr::from(8), Fr::from(2)];
let ivc_inputs_var =
Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs_public)).unwrap();
let external_inputs_var =
Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs_private)).unwrap();
let z_i1 = circuit
.generate_step_constraints(cs.clone(), 0, ivc_inputs_var, external_inputs_var)
.unwrap();
assert!(cs.is_satisfied().unwrap());
assert_eq!(z_i1[0].value().unwrap(), Fr::from(10_u8));
assert_eq!(z_i1[1].value().unwrap(), Fr::from(10_u8));
}
#[test]
fn test_generate_constraints_no_external_inputs() {
let cs = ConstraintSystem::<Fr>::new_ref();
let params = (NONAME_CIRCUIT_NO_EXTERNAL_INPUTS.to_owned(), 2, 0);
let inputs_public = vec![Fr::from(2), Fr::from(5)];
let ivc_inputs_var =
Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(inputs_public)).unwrap();
let f_circuit = NonameFCircuit::<Fr, R1csBn254Field>::new(params).unwrap();
f_circuit
.generate_step_constraints(cs.clone(), 0, ivc_inputs_var, vec![])
.unwrap();
assert!(cs.is_satisfied().unwrap());
}
}

View File

@@ -0,0 +1,58 @@
use std::collections::HashMap;
use ark_ff::PrimeField;
use ark_r1cs_std::{fields::fp::FpVar, R1CSVar};
use ark_relations::r1cs::SynthesisError;
use noname::inputs::JsonInputs;
use serde_json::json;
pub struct NonameInputs(pub JsonInputs);
impl<F: PrimeField> From<(&Vec<F>, String)> for NonameInputs {
fn from(value: (&Vec<F>, String)) -> Self {
let (values, key) = value;
let mut inputs = HashMap::new();
if values.is_empty() {
NonameInputs(JsonInputs(inputs))
} else {
let field_elements: Vec<String> = values
.iter()
.map(|value| {
if value.is_zero() {
"0".to_string()
} else {
value.to_string()
}
})
.collect();
inputs.insert(key, json!(field_elements));
NonameInputs(JsonInputs(inputs))
}
}
}
impl NonameInputs {
pub fn from_fpvars<F: PrimeField>(
value: (&Vec<FpVar<F>>, String),
) -> Result<Self, SynthesisError> {
let (values, key) = value;
let mut inputs = HashMap::new();
if values.is_empty() {
Ok(NonameInputs(JsonInputs(inputs)))
} else {
let field_elements: Vec<String> = values
.iter()
.map(|var| {
let value = var.value()?;
if value.is_zero() {
Ok("0".to_string())
} else {
Ok(value.to_string())
}
})
.collect::<Result<Vec<String>, SynthesisError>>()?;
inputs.insert(key, json!(field_elements));
Ok(NonameInputs(JsonInputs(inputs)))
}
}
}