From 18a3e0aa9303036c92abf4d299ba50327e95f9d2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sat, 3 Aug 2024 07:49:51 +0200 Subject: [PATCH] feat: fold noir circuits, add an e2e example, tests, a `compile.sh` script and update CI (#131) --- .github/workflows/ci.yml | 10 + .gitignore | 3 + examples/noir_full_flow.rs | 147 +++++++++ folding-schemes/Cargo.toml | 2 + folding-schemes/src/frontend/mod.rs | 1 + folding-schemes/src/frontend/noir/mod.rs | 299 ++++++++++++++++++ .../src/frontend/noir/test_folder/compile.sh | 7 + .../noir/test_folder/test_circuit/Nargo.toml | 8 + .../noir/test_folder/test_circuit/src/main.nr | 11 + .../noir/test_folder/test_mimc/Nargo.toml | 8 + .../noir/test_folder/test_mimc/src/main.nr | 6 + .../test_no_external_inputs/Nargo.toml | 8 + .../test_no_external_inputs/src/main.nr | 9 + solidity-verifiers/Cargo.toml | 5 + 14 files changed, 524 insertions(+) create mode 100644 examples/noir_full_flow.rs create mode 100644 folding-schemes/src/frontend/noir/mod.rs create mode 100755 folding-schemes/src/frontend/noir/test_folder/compile.sh create mode 100644 folding-schemes/src/frontend/noir/test_folder/test_circuit/Nargo.toml create mode 100644 folding-schemes/src/frontend/noir/test_folder/test_circuit/src/main.nr create mode 100644 folding-schemes/src/frontend/noir/test_folder/test_mimc/Nargo.toml create mode 100644 folding-schemes/src/frontend/noir/test_folder/test_mimc/src/main.nr create mode 100644 folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/Nargo.toml create mode 100644 folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/src/main.nr diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31f7745..0fcf108 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,9 @@ jobs: - feature: default steps: - uses: actions/checkout@v2 + - uses: noir-lang/noirup@v0.1.3 + with: + toolchain: nightly - uses: actions-rs/toolchain@v1 # use the more efficient nextest - uses: taiki-e/install-action@nextest @@ -61,6 +64,8 @@ jobs: chmod +x /usr/local/bin/solc - name: Execute compile.sh to generate .r1cs and .wasm from .circom run: ./folding-schemes/src/frontend/circom/test_folder/compile.sh + - name: Execute compile.sh to generate .json from noir + run: ./folding-schemes/src/frontend/noir/test_folder/compile.sh - name: Build # This build will be reused by nextest, # and also checks (--all-targets) that benches don't bit-rot @@ -79,6 +84,9 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 + - uses: noir-lang/noirup@v0.1.3 + with: + toolchain: nightly - name: Download Circom run: | mkdir -p $HOME/bin @@ -91,6 +99,8 @@ jobs: chmod +x /usr/local/bin/solc - name: Execute compile.sh to generate .r1cs and .wasm from .circom run: ./folding-schemes/src/frontend/circom/test_folder/compile.sh + - name: Execute compile.sh to generate .json from noir + run: ./folding-schemes/src/frontend/noir/test_folder/compile.sh - name: Run examples tests run: cargo test --examples - name: Run examples diff --git a/.gitignore b/.gitignore index d894878..d38766b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ folding-schemes/src/frontend/circom/test_folder/*_js/ *.r1cs *.sym +# Noir generated files +folding-schemes/src/frontend/noir/test_folder/*/target/* + # generated contracts at test time solidity-verifiers/generated examples/*.sol diff --git a/examples/noir_full_flow.rs b/examples/noir_full_flow.rs new file mode 100644 index 0000000..1d5ba52 --- /dev/null +++ b/examples/noir_full_flow.rs @@ -0,0 +1,147 @@ +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(clippy::upper_case_acronyms)] +/// +/// This example performs the full flow: +/// - define the circuit to be folded +/// - fold the circuit with Nova+CycleFold's IVC +/// - generate a DeciderEthCircuit final proof +/// - generate the Solidity contract that verifies the proof +/// - verify the proof in the EVM +/// +use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as G1}; + +use ark_groth16::Groth16; +use ark_grumpkin::{constraints::GVar as GVar2, Projective as G2}; + +use folding_schemes::{ + commitment::{kzg::KZG, pedersen::Pedersen}, + folding::nova::{ + decider_eth::{prepare_calldata, Decider as DeciderEth}, + Nova, PreprocessorParam, + }, + frontend::{ + noir::{load_noir_circuit, NoirFCircuit}, + FCircuit, + }, + transcript::poseidon::poseidon_canonical_config, + Decider, FoldingScheme, +}; +use std::{env, time::Instant}; + +use solidity_verifiers::{ + evm::{compile_solidity, Evm}, + utils::get_function_selector_for_nova_cyclefold_verifier, + verifiers::nova_cyclefold::get_decider_template_for_cyclefold_decider, + NovaCycleFoldVerifierKey, +}; + +fn main() { + // set the initial state + let z_0 = vec![Fr::from(1)]; + + // initialize the noir fcircuit + let cur_path = env::current_dir().unwrap(); + + let circuit_path = format!( + "{}/folding-schemes/src/frontend/noir/test_folder/test_mimc/target/test_mimc.json", + cur_path.to_str().unwrap() + ); + + let circuit = load_noir_circuit(circuit_path); + let f_circuit = NoirFCircuit { + circuit, + state_len: 1, + external_inputs_len: 0, + }; + + pub type N = Nova, KZG<'static, Bn254>, Pedersen>; + pub type D = DeciderEth< + G1, + GVar, + G2, + GVar2, + NoirFCircuit, + KZG<'static, Bn254>, + Pedersen, + Groth16, + N, + >; + + let poseidon_config = poseidon_canonical_config::(); + 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 our case we use Nova + let mut nova = N::init(&nova_params, f_circuit.clone(), z_0).unwrap(); + + // prepare the Decider prover & verifier params + let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); + + // run n steps of the folding iteration + for i in 0..5 { + let start = Instant::now(); + nova.prove_step(rng, vec![], None).unwrap(); + println!("Nova::prove_step {}: {:?}", i, start.elapsed()); + } + + let start = Instant::now(); + let proof = D::prove(rng, decider_pp, nova.clone()).unwrap(); + println!("generated Decider proof: {:?}", start.elapsed()); + + let verified = D::verify( + decider_vp.clone(), + nova.i, + nova.z_0.clone(), + nova.z_i.clone(), + &nova.U_i, + &nova.u_i, + &proof, + ) + .unwrap(); + assert!(verified); + println!("Decider proof verification: {}", verified); + + // Now, let's generate the Solidity code that verifies this Decider final proof + let function_selector = + get_function_selector_for_nova_cyclefold_verifier(nova.z_0.len() * 2 + 1); + + let calldata: Vec = prepare_calldata( + function_selector, + nova.i, + nova.z_0, + nova.z_i, + &nova.U_i, + &nova.u_i, + proof, + ) + .unwrap(); + + // prepare the setup params for the solidity verifier + 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); + + // verify the proof against the solidity code in the EVM + let nova_cyclefold_verifier_bytecode = compile_solidity(&decider_solidity_code, "NovaDecider"); + let mut evm = Evm::default(); + let verifier_address = evm.create(nova_cyclefold_verifier_bytecode); + 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(); + let s = solidity_verifiers::utils::get_formatted_calldata(calldata.clone()); + fs::write("./examples/solidity-calldata.inputs", s.join(",\n")).expect(""); +} diff --git a/folding-schemes/Cargo.toml b/folding-schemes/Cargo.toml index 9c9d4b9..7a133ed 100644 --- a/folding-schemes/Cargo.toml +++ b/folding-schemes/Cargo.toml @@ -28,6 +28,8 @@ ark-noname = { git = "https://github.com/dmpierre/ark-noname", branch="feat/sono noname = { git = "https://github.com/dmpierre/noname" } serde_json = "1.0.85" # to (de)serialize JSON serde = "1.0.203" +acvm = { git = "https://github.com/noir-lang/noir", rev="2b4853e", default-features = false } +arkworks_backend = { git = "https://github.com/dmpierre/arkworks_backend", branch="feat/sonobe-integration" } # tmp import for espresso's sumcheck espresso_subroutines = {git="https://github.com/EspressoSystems/hyperplonk", package="subroutines"} diff --git a/folding-schemes/src/frontend/mod.rs b/folding-schemes/src/frontend/mod.rs index e8afa3f..f1f73d3 100644 --- a/folding-schemes/src/frontend/mod.rs +++ b/folding-schemes/src/frontend/mod.rs @@ -5,6 +5,7 @@ use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; use ark_std::fmt::Debug; pub mod circom; +pub mod noir; pub mod noname; /// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie. diff --git a/folding-schemes/src/frontend/noir/mod.rs b/folding-schemes/src/frontend/noir/mod.rs new file mode 100644 index 0000000..f069c22 --- /dev/null +++ b/folding-schemes/src/frontend/noir/mod.rs @@ -0,0 +1,299 @@ +use std::collections::HashMap; + +use crate::Error; + +use super::FCircuit; +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 arkworks_backend::{read_program_from_file, sonobe_bridge::AcirCircuitSonobe}; + +#[derive(Clone, Debug)] +pub struct NoirFCircuit { + pub circuit: Circuit>, + pub state_len: usize, + pub external_inputs_len: usize, +} + +impl FCircuit for NoirFCircuit { + type Params = (String, usize, usize); + + fn new(params: Self::Params) -> Result { + let (path, state_len, external_inputs_len) = params; + let program = + read_program_from_file(path).map_err(|ee| Error::Other(format!("{:?}", ee)))?; + let circuit: Circuit> = 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, + external_inputs: Vec, // inputs that are not part of the state + ) -> Result, crate::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::::try_from_str(&value) + .ok_or(SynthesisError::Unsatisfiable)?; + acvm.overwrite_witness(witness, f); + Ok(()) + }) + .collect::, 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::::try_from_str(&value) + .ok_or(SynthesisError::Unsatisfiable)?; + acvm.overwrite_witness(AcvmWitness(witness.witness_index()), f); + Ok(()) + }) + .collect::, 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::, SynthesisError>>()?; + + Ok(assigned_z_i1) + } + + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef, + _i: usize, + z_i: Vec>, + external_inputs: Vec>, // inputs that are not part of the state + ) -> Result>, 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::::try_from_str(&value) + .ok_or(SynthesisError::Unsatisfiable)?; + acvm.overwrite_witness(witness, f); + Ok(()) + }) + .collect::, 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::::try_from_str(&value) + .ok_or(SynthesisError::Unsatisfiable)?; + acvm.overwrite_witness(witness, f); + Ok(()) + }) + .collect::, 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::::new_witness(cs.clone(), || Ok(noir_field_element.into_repr())) + }) + .collect::>, 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(path: String) -> Circuit> { + let program: Program> = read_program_from_file(path).unwrap(); + let circuit: Circuit> = program.functions[0].clone(); + circuit +} + +#[cfg(test)] +mod tests { + use crate::frontend::{noir::load_noir_circuit, FCircuit}; + use ark_bn254::Fr; + use ark_r1cs_std::R1CSVar; + use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar}; + use ark_relations::r1cs::ConstraintSystem; + use std::env; + + use crate::frontend::noir::NoirFCircuit; + + #[test] + fn test_step_native() { + let cur_path = env::current_dir().unwrap(); + let circuit_path = format!( + "{}/src/frontend/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::::new_ref(); + let cur_path = env::current_dir().unwrap(); + let circuit_path = format!( + "{}/src/frontend/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::>::new_witness(cs.clone(), || Ok(inputs.clone())).unwrap(); + let external_inputs = Vec::>::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::::new_ref(); + let cur_path = env::current_dir().unwrap(); + let circuit_path = format!( + "{}/src/frontend/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::>::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)); + } +} diff --git a/folding-schemes/src/frontend/noir/test_folder/compile.sh b/folding-schemes/src/frontend/noir/test_folder/compile.sh new file mode 100755 index 0000000..413d2a9 --- /dev/null +++ b/folding-schemes/src/frontend/noir/test_folder/compile.sh @@ -0,0 +1,7 @@ +#!/bin/bash +CUR_DIR=$(pwd) +TEST_PATH="${CUR_DIR}/folding-schemes/src/frontend/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 diff --git a/folding-schemes/src/frontend/noir/test_folder/test_circuit/Nargo.toml b/folding-schemes/src/frontend/noir/test_folder/test_circuit/Nargo.toml new file mode 100644 index 0000000..69429a8 --- /dev/null +++ b/folding-schemes/src/frontend/noir/test_folder/test_circuit/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "test_circuit" +type = "bin" +authors = [""] +compiler_version = ">=0.30.0" + +[dependencies] + diff --git a/folding-schemes/src/frontend/noir/test_folder/test_circuit/src/main.nr b/folding-schemes/src/frontend/noir/test_folder/test_circuit/src/main.nr new file mode 100644 index 0000000..4e0c90a --- /dev/null +++ b/folding-schemes/src/frontend/noir/test_folder/test_circuit/src/main.nr @@ -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] +} diff --git a/folding-schemes/src/frontend/noir/test_folder/test_mimc/Nargo.toml b/folding-schemes/src/frontend/noir/test_folder/test_mimc/Nargo.toml new file mode 100644 index 0000000..f946848 --- /dev/null +++ b/folding-schemes/src/frontend/noir/test_folder/test_mimc/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "test_mimc" +type = "bin" +authors = [""] +compiler_version = ">=0.30.0" + +[dependencies] + diff --git a/folding-schemes/src/frontend/noir/test_folder/test_mimc/src/main.nr b/folding-schemes/src/frontend/noir/test_folder/test_mimc/src/main.nr new file mode 100644 index 0000000..2670d3d --- /dev/null +++ b/folding-schemes/src/frontend/noir/test_folder/test_mimc/src/main.nr @@ -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 +} diff --git a/folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/Nargo.toml b/folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/Nargo.toml new file mode 100644 index 0000000..22373d3 --- /dev/null +++ b/folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "test_no_external_inputs" +type = "bin" +authors = [""] +compiler_version = ">=0.30.0" + +[dependencies] + diff --git a/folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/src/main.nr b/folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/src/main.nr new file mode 100644 index 0000000..26f6cd7 --- /dev/null +++ b/folding-schemes/src/frontend/noir/test_folder/test_no_external_inputs/src/main.nr @@ -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] +} + diff --git a/solidity-verifiers/Cargo.toml b/solidity-verifiers/Cargo.toml index 0252c14..907f93b 100644 --- a/solidity-verifiers/Cargo.toml +++ b/solidity-verifiers/Cargo.toml @@ -52,3 +52,8 @@ path = "../examples/circom_full_flow.rs" [[example]] name = "noname_full_flow" path = "../examples/noname_full_flow.rs" + +[[example]] +name = "noir_full_flow" +path = "../examples/noir_full_flow.rs" +