feat: fold noir circuits, add an e2e example, tests, a `compile.sh` script and update CI (#131)
Pierre pdaixmoreux@gmail.com
Sat, 03 Aug 2024 07:49:51 +0200
14 files changed,
524 insertions(+),
0 deletions(-)
jump to
M
.github/workflows/ci.yml
→
.github/workflows/ci.yml
@@ -45,6 +45,9 @@ include:
- 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 @@ curl -sSfL https://github.com/ethereum/solidity/releases/download/v0.8.4/solc-static-linux -o /usr/local/bin/solc
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 @@ runs-on: ubuntu-latest
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 @@ curl -sSfL https://github.com/ethereum/solidity/releases/download/v0.8.4/solc-static-linux -o /usr/local/bin/solc
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
M
.gitignore
→
.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
A
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<G1, GVar, G2, GVar2, NoirFCircuit<Fr>, KZG<'static, Bn254>, Pedersen<G2>>; + pub type D = DeciderEth< + G1, + GVar, + G2, + GVar2, + NoirFCircuit<Fr>, + KZG<'static, Bn254>, + Pedersen<G2>, + Groth16<Bn254>, + N, + >; + + let poseidon_config = poseidon_canonical_config::<Fr>(); + 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<u8> = 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(""); +}
M
folding-schemes/Cargo.toml
→
folding-schemes/Cargo.toml
@@ -28,6 +28,8 @@ 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 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"}
M
folding-schemes/src/frontend/mod.rs
→
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.
A
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<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 = (String, usize, usize); + + fn new(params: Self::Params) -> Result<Self, crate::Error> { + 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<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>, 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::<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::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::<Fr>::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::<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/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::<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)); + } +}
A
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
A
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] +
A
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] +}
A
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] +
A
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 +}
A
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] +
A
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] +} +
M
solidity-verifiers/Cargo.toml
→
solidity-verifiers/Cargo.toml
@@ -52,3 +52,8 @@
[[example]] name = "noname_full_flow" path = "../examples/noname_full_flow.rs" + +[[example]] +name = "noir_full_flow" +path = "../examples/noir_full_flow.rs" +