Skip to content

Commit

Permalink
Adapt Frontend/Circom for FCircuit Trait (#71)
Browse files Browse the repository at this point in the history
* initial

* improved z_i

* improved

* Redefined the .circom and the test code for CircomWrapper

* added test function for wrapper_circuit which has circom_fcircuit, but incompatibility error of Pairing and Pallas

* changed the path in the gitignore

* Remove circom generated path

* improved variable vector

* Pallas::Fr

* improved to the primefield

* mentioned the issues:1. extract z_i1(only public output) 2.constraintsSystem and its Ref

* modified

* public input in circom

* generalized; removed the hardcorded value

* Generalization using FpVar::<Fr>::new_input

* initial

* improved z_i

* improved

* Redefined the .circom and the test code for CircomWrapper

* added test function for wrapper_circuit which has circom_fcircuit, but incompatibility error of Pairing and Pallas

* Remove circom generated path

* improved variable vector

* Pallas::Fr

* improved to the primefield

* mentioned the issues:1. extract z_i1(only public output) 2.constraintsSystem and its Ref

* modified

* Small updates:

- update cubic_circuit.circom: remove extra constraint, remove public
  inputs
- remove allocations of inputs in arkworks
- add return of z_{i+1} at the end of CircomtoFCircuit::generate_step_constraints

With this tmp fix the `test_circom_step_constraints` passes, but needs to be iterated and polished.

* Update circom-compat to re-allocate inputs

* update after rebase to latest main: add usage of self.state_len() to circom frontend

* move circom frontend related structs into frontend/circom dir

* clippy lints

* extract_witness

* add comments

* clean

* fmt, lint, and spell

* CI Check trigger

* fmt

* applied the feedback

---------

Co-authored-by: Y5 <76672645+yugonsan@users.noreply.github.com>
Co-authored-by: arnaucube <root@arnaucube.com>
  • Loading branch information
3 people authored Apr 14, 2024
1 parent 4dcb981 commit 03f6691
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 217 deletions.
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/target
Cargo.lock
# Circom generated files
/src/frontend/circom/test_folder/test_circuit.r1cs
/src/frontend/circom/test_folder/test_circuit_js/
folding-schemes/src/frontend/circom/test_folder/cubic_circuit.r1cs
folding-schemes/src/frontend/circom/test_folder/cubic_circuit_js/

# generated contracts at test time
solidity-verifiers/generated
solidity-verifiers/generated
2 changes: 1 addition & 1 deletion folding-schemes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ ark-relations = { version = "^0.4.0", default-features = false }
ark-r1cs-std = { version = "0.4.0", default-features = false } # this is patched at the workspace level
ark-snark = { version = "^0.4.0"}
ark-serialize = "^0.4.0"
ark-circom = { git = "/~https://github.com/gakonst/ark-circom.git" }
ark-circom = { git = "/~https://github.com/arnaucube/circom-compat.git" }
thiserror = "1.0"
rayon = "1.7.0"
num-bigint = "0.4"
Expand Down
343 changes: 147 additions & 196 deletions folding-schemes/src/frontend/circom/mod.rs
Original file line number Diff line number Diff line change
@@ -1,226 +1,177 @@
use std::{error::Error, fs::File, io::BufReader, marker::PhantomData, path::PathBuf};

use color_eyre::Result;
use ark_circom::circom::CircomCircuit;
use ark_ff::PrimeField;
use ark_r1cs_std::alloc::AllocVar;
use ark_r1cs_std::fields::fp::FpVar;
use ark_r1cs_std::R1CSVar;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
use ark_std::fmt::Debug;
use num_bigint::BigInt;
use std::path::PathBuf;

use ark_circom::{circom::r1cs_reader, WitnessCalculator};
use ark_ec::pairing::Pairing;
use ark_ff::PrimeField;
use crate::frontend::FCircuit;
use crate::Error;

use crate::ccs::r1cs::R1CS;
use crate::utils::vec::SparseMatrix;

// Define the sparse matrices on PrimeFiled.
pub type Constraints<F> = (ConstraintVec<F>, ConstraintVec<F>, ConstraintVec<F>);
pub type ConstraintVec<F> = Vec<(usize, F)>;
type ExtractedConstraints<F> = (Vec<Constraints<F>>, usize, usize);
pub type ExtractedConstraintsResult<F> = Result<ExtractedConstraints<F>, Box<dyn Error>>;
pub type R1CSandZ<F> = (R1CS<F>, Vec<F>);

// A struct that wraps Circom functionalities, allowing for extraction of R1CS and witnesses
// based on file paths to Circom's .r1cs and .wasm.
pub struct CircomWrapper<E: Pairing> {
r1cs_filepath: PathBuf,
wasm_filepath: PathBuf,
_marker: PhantomData<E>,
pub mod utils;
use utils::CircomWrapper;

/// Define CircomFCircuit
#[derive(Clone, Debug)]
pub struct CircomFCircuit<F: PrimeField> {
circom_wrapper: CircomWrapper<F>,
}

impl<E: Pairing> CircomWrapper<E> {
// Creates a new instance of the CircomWrapper with the file paths.
pub fn new(r1cs_filepath: PathBuf, wasm_filepath: PathBuf) -> Self {
CircomWrapper {
r1cs_filepath,
wasm_filepath,
_marker: PhantomData,
}
impl<F: PrimeField> FCircuit<F> for CircomFCircuit<F> {
type Params = (PathBuf, PathBuf);

fn new(params: Self::Params) -> Self {
let (r1cs_path, wasm_path) = params;
let circom_wrapper = CircomWrapper::new(r1cs_path, wasm_path);
Self { circom_wrapper }
}

// Aggregates multiple functions to obtain R1CS and Z as defined in folding-schemes from Circom.
pub fn extract_r1cs_and_z(
&self,
inputs: &[(String, Vec<BigInt>)],
) -> Result<R1CSandZ<E::ScalarField>, Box<dyn Error>> {
let (constraints, pub_io_len, num_variables) = self.extract_constraints_from_r1cs()?;
let witness = self.calculate_witness(inputs)?;
self.circom_to_folding_schemes_r1cs_and_z(constraints, &witness, pub_io_len, num_variables)
fn state_len(&self) -> usize {
1
}

// Extracts constraints from the r1cs file.
pub fn extract_constraints_from_r1cs(&self) -> ExtractedConstraintsResult<E::ScalarField>
where
E: Pairing,
{
// Opens the .r1cs file and create a reader.
let file = File::open(&self.r1cs_filepath)?;
let reader = BufReader::new(file);

// Reads the R1CS file and extract the constraints directly.
let r1cs_file = r1cs_reader::R1CSFile::<E>::new(reader)?;
let pub_io_len = (r1cs_file.header.n_pub_in + r1cs_file.header.n_pub_out) as usize;
let r1cs = r1cs_reader::R1CS::<E>::from(r1cs_file);
let num_variables = r1cs.num_variables;
let constraints: Vec<Constraints<E::ScalarField>> = r1cs.constraints;

Ok((constraints, pub_io_len, num_variables))
fn step_native(&self, _i: usize, z_i: Vec<F>) -> Result<Vec<F>, Error> {
// Converts PrimeField values to BigInt for computing witness.
let input_num_bigint = z_i
.iter()
.map(|val| self.circom_wrapper.ark_primefield_to_num_bigint(*val))
.collect::<Vec<BigInt>>();

// Computes witness
let witness = self
.circom_wrapper
.extract_witness(&[("ivc_input".to_string(), input_num_bigint)])
.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)
}

// Converts a set of constraints from ark-circom into R1CS format of folding-schemes.
pub fn convert_to_folding_schemes_r1cs<F>(
fn generate_step_constraints(
&self,
constraints: Vec<Constraints<F>>,
pub_io_len: usize,
num_variables: usize,
) -> R1CS<F>
where
F: PrimeField,
{
let mut a_matrix: Vec<Vec<(F, usize)>> = Vec::new();
let mut b_matrix: Vec<Vec<(F, usize)>> = Vec::new();
let mut c_matrix: Vec<Vec<(F, usize)>> = Vec::new();

let n_rows = constraints.len();

for (ai, bi, ci) in constraints {
a_matrix.push(
ai.into_iter()
.map(|(index, scalar)| (scalar, index))
.collect(),
);
b_matrix.push(
bi.into_iter()
.map(|(index, scalar)| (scalar, index))
.collect(),
);
c_matrix.push(
ci.into_iter()
.map(|(index, scalar)| (scalar, index))
.collect(),
);
cs: ConstraintSystemRef<F>,
_i: usize,
z_i: Vec<FpVar<F>>,
) -> Result<Vec<FpVar<F>>, SynthesisError> {
let mut input_values = Vec::new();
// Converts each FpVar to PrimeField value, then to num_bigint::BigInt.
for fp_var in z_i.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);
}

let l = pub_io_len;
let n_cols = num_variables;
let num_bigint_inputs = vec![("ivc_input".to_string(), input_values)];

let A = SparseMatrix {
n_rows,
n_cols,
coeffs: a_matrix,
};
let B = SparseMatrix {
n_rows,
n_cols,
coeffs: b_matrix,
};
let C = SparseMatrix {
n_rows,
n_cols,
coeffs: c_matrix,
// Extracts R1CS and witness.
let (r1cs, witness) = self
.circom_wrapper
.extract_r1cs_and_witness(&num_bigint_inputs)
.map_err(|_| SynthesisError::AssignmentMissing)?;

// Initializes the CircomCircuit.
let circom_circuit = CircomCircuit {
r1cs,
witness: witness.clone(),
inputs_already_computed: true,
};

R1CS::<F> { l, A, B, C }
}
// Generates the constraints for the circom_circuit.
circom_circuit
.generate_constraints(cs.clone())
.map_err(|_| SynthesisError::AssignmentMissing)?;

// Calculates the witness given the Wasm filepath and inputs.
pub fn calculate_witness(&self, inputs: &[(String, Vec<BigInt>)]) -> Result<Vec<BigInt>> {
let mut calculator = WitnessCalculator::new(&self.wasm_filepath)?;
calculator.calculate_witness(inputs.iter().cloned(), true)
}
// Checks for constraint satisfaction.
if !cs.is_satisfied().unwrap() {
return Err(SynthesisError::Unsatisfiable);
}

// Converts a num_bigint input to `PrimeField`'s BigInt.
pub fn num_bigint_to_ark_bigint<F: PrimeField>(
&self,
value: &BigInt,
) -> Result<F::BigInt, Box<dyn Error>> {
let big_uint = value
.to_biguint()
.ok_or_else(|| "BigInt is negative".to_string())?;
F::BigInt::try_from(big_uint).map_err(|_| "BigInt conversion failed".to_string().into())
}
let w = witness.ok_or(SynthesisError::Unsatisfiable)?;

// Converts R1CS constraints and witness from ark-circom format
// into folding-schemes R1CS and z format.
pub fn circom_to_folding_schemes_r1cs_and_z<F>(
&self,
constraints: Vec<Constraints<F>>,
witness: &[BigInt],
pub_io_len: usize,
num_variables: usize,
) -> Result<(R1CS<F>, Vec<F>), Box<dyn Error>>
where
F: PrimeField,
{
let folding_schemes_r1cs =
self.convert_to_folding_schemes_r1cs(constraints, pub_io_len, num_variables);

let z: Result<Vec<F>, _> = witness
.iter()
.map(|big_int| {
let ark_big_int = self.num_bigint_to_ark_bigint::<F>(big_int)?;
F::from_bigint(ark_big_int).ok_or_else(|| {
"Failed to convert bigint to field element"
.to_string()
.into()
})
})
.collect();

z.map(|z| (folding_schemes_r1cs, z))
// Extracts the z_i1(next state) from the witness vector.
let z_i1: Vec<FpVar<F>> =
Vec::<FpVar<F>>::new_witness(cs.clone(), || Ok(w[1..1 + self.state_len()].to_vec()))?;

Ok(z_i1)
}
}

#[cfg(test)]
mod tests {
use crate::frontend::circom::CircomWrapper;
use ark_bn254::Bn254;
use num_bigint::BigInt;
use std::env;

/*
test_circuit represents 35 = x^3 + x + 5 in test_folder/test_circuit.circom.
In the test_circom_conversion_success function, x is assigned the value 3, which satisfies the R1CS correctly.
In the test_circom_conversion_failure function, x is assigned the value 6, which doesn't satisfy the R1CS.
*/

/*
To generate .r1cs and .wasm files, run the below command in the terminal.
bash ./src/frontend/circom/test_folder/compile.sh
*/

fn test_circom_conversion_logic(expect_success: bool, inputs: Vec<(String, Vec<BigInt>)>) {
let current_dir = env::current_dir().unwrap();
let base_path = current_dir.join("src/frontend/circom/test_folder");

let r1cs_filepath = base_path.join("test_circuit.r1cs");
let wasm_filepath = base_path.join("test_circuit_js/test_circuit.wasm");

assert!(r1cs_filepath.exists());
assert!(wasm_filepath.exists());

let circom_wrapper = CircomWrapper::<Bn254>::new(r1cs_filepath, wasm_filepath);

let (r1cs, z) = circom_wrapper
.extract_r1cs_and_z(&inputs)
.expect("Error processing input");

// Checks the relationship of R1CS.
let check_result = std::panic::catch_unwind(|| {
r1cs.check_relation(&z).unwrap();
});

match (expect_success, check_result) {
(true, Ok(_)) => {}
(false, Err(_)) => {}
(true, Err(_)) => panic!("Expected success but got a failure."),
(false, Ok(_)) => panic!("Expected a failure but got success."),
}
pub mod tests {
use super::*;
use ark_pallas::Fr;
use ark_r1cs_std::alloc::AllocVar;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem};

// Tests the step_native function of CircomFCircuit.
#[test]
fn test_circom_step_native() {
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 circom_fcircuit = CircomFCircuit::<Fr>::new((r1cs_path, wasm_path));

let z_i = vec![Fr::from(3u32)];
let z_i1 = circom_fcircuit.step_native(1, z_i).unwrap();
assert_eq!(z_i1, vec![Fr::from(35u32)]);
}

// Tests the generate_step_constraints function of CircomFCircuit.
#[test]
fn test_circom_conversion() {
// expect it to pass for correct inputs.
test_circom_conversion_logic(true, vec![("step_in".to_string(), vec![BigInt::from(3)])]);
fn test_circom_step_constraints() {
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 circom_fcircuit = CircomFCircuit::<Fr>::new((r1cs_path, wasm_path));

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 cs = ConstraintSystem::<Fr>::new_ref();
let z_i1_var = circom_fcircuit
.generate_step_constraints(cs.clone(), 1, z_i_var)
.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/frontend/circom/test_folder/cubic_circuit.r1cs");
let wasm_path =
PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm");

let circom_fcircuit = CircomFCircuit::<Fr>::new((r1cs_path, wasm_path));

// Allocates z_i1 by using step_native function.
let z_i = vec![Fr::from(3_u32)];
let wrapper_circuit = crate::frontend::tests::WrapperCircuit {
FC: circom_fcircuit.clone(),
z_i: Some(z_i.clone()),
z_i1: Some(circom_fcircuit.step_native(0, z_i.clone()).unwrap()),
};

let cs = ConstraintSystem::<Fr>::new_ref();

// expect it to fail for incorrect inputs.
test_circom_conversion_logic(false, vec![("step_in".to_string(), vec![BigInt::from(7)])]);
wrapper_circuit.generate_constraints(cs.clone()).unwrap();
assert!(
cs.is_satisfied().unwrap(),
"Constraint system is not satisfied"
);
}
}
Loading

0 comments on commit 03f6691

Please sign in to comment.