From cc1f6316a7baba4ab1eebb63bc98fefec3514dc2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 12 Jul 2024 22:04:20 +0200 Subject: [PATCH] feat: add noname as a frontend to sonobe (#121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add noname as a frontend to sonobe fix: remove extra `rng` usage Co-authored-by: Carlos Pérez <37264926+CPerezz@users.noreply.github.com> * Update README.md Co-authored-by: arnaucube * chore: move ark-noname to dev dependencies in solidity-verifiers cargo --------- Co-authored-by: Carlos Pérez <37264926+CPerezz@users.noreply.github.com> Co-authored-by: arnaucube --- README.md | 1 + examples/noname_full_flow.rs | 158 +++++++++++++++ folding-schemes/Cargo.toml | 6 +- folding-schemes/src/frontend/mod.rs | 1 + folding-schemes/src/frontend/noname/mod.rs | 201 +++++++++++++++++++ folding-schemes/src/frontend/noname/utils.rs | 58 ++++++ folding-schemes/src/lib.rs | 4 +- solidity-verifiers/Cargo.toml | 5 + 8 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 examples/noname_full_flow.rs create mode 100644 folding-schemes/src/frontend/noname/mod.rs create mode 100644 folding-schemes/src/frontend/noname/utils.rs diff --git a/README.md b/README.md index 7af21f4..cc38015 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Available frontends to define the folded circuit: - [arkworks](https://github.com/arkworks-rs), arkworks contributors - [Circom](https://github.com/iden3/circom), iden3, 0Kims Association +- [Noname](https://github.com/zksecurity/noname), zkSecurity ## Usage diff --git a/examples/noname_full_flow.rs b/examples/noname_full_flow.rs new file mode 100644 index 0000000..6d84aa0 --- /dev/null +++ b/examples/noname_full_flow.rs @@ -0,0 +1,158 @@ +#![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 noname::backends::r1cs::R1csBn254Field; + +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::{noname::NonameFCircuit, FCircuit}, + transcript::poseidon::poseidon_canonical_config, + Decider, FoldingScheme, +}; +use std::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() { + 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]; +}"; + + // set the initial state + let z_0 = vec![Fr::from(2), Fr::from(5)]; + + // set the external inputs to be used at each step of the IVC, it has length of 10 since this + // is the number of steps that we will do + let external_inputs = vec![ + vec![Fr::from(8u32), Fr::from(2u32)], + vec![Fr::from(40), Fr::from(5)], + ]; + + // initialize the noname circuit + let f_circuit_params = (NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(), 2, 2); + let f_circuit = NonameFCircuit::::new(f_circuit_params).unwrap(); + + pub type N = Nova< + G1, + GVar, + G2, + GVar2, + NonameFCircuit, + KZG<'static, Bn254>, + Pedersen, + >; + pub type D = DeciderEth< + G1, + GVar, + G2, + GVar2, + NonameFCircuit, + 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.clone(), 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, external_inputs_at_step) in external_inputs.iter().enumerate() { + let start = Instant::now(); + nova.prove_step(rng, external_inputs_at_step.clone()) + .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 a309293..9c9d4b9 100644 --- a/folding-schemes/Cargo.toml +++ b/folding-schemes/Cargo.toml @@ -24,8 +24,12 @@ color-eyre = "=0.6.2" ark-bn254 = {version="0.4.0"} ark-groth16 = { version = "^0.4.0" } sha3 = "0.10" +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" -# tmp imports for espresso's sumcheck +# tmp import for espresso's sumcheck espresso_subroutines = {git="https://github.com/EspressoSystems/hyperplonk", package="subroutines"} [dev-dependencies] diff --git a/folding-schemes/src/frontend/mod.rs b/folding-schemes/src/frontend/mod.rs index 15fcc74..e8afa3f 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 noname; /// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie. /// inside the agmented F' function). diff --git a/folding-schemes/src/frontend/noname/mod.rs b/folding-schemes/src/frontend/noname/mod.rs new file mode 100644 index 0000000..61ac1e7 --- /dev/null +++ b/folding-schemes/src/frontend/noname/mod.rs @@ -0,0 +1,201 @@ +use crate::Error; +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 super::FCircuit; +use ark_ff::PrimeField; +use ark_noname::utils::compile_source_code; +use noname::backends::{r1cs::R1CS as R1CSNoname, BackendField}; +use noname::witness::CompiledCircuit; +pub mod utils; +#[derive(Debug, Clone)] +pub struct NonameFCircuit { + pub state_len: usize, + pub external_inputs_len: usize, + pub circuit: CompiledCircuit>, + _f: PhantomData, +} + +impl FCircuit for NonameFCircuit { + type Params = (String, usize, usize); + + fn new(params: Self::Params) -> Result { + let (code, state_len, external_inputs_len) = params; + let compiled_circuit = compile_source_code::(&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, + external_inputs: Vec, + ) -> Result, crate::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, + _i: usize, + z_i: Vec>, + external_inputs: Vec>, + ) -> Result>, 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> = (1..z_i1_end_index) + .map(|idx| -> Result, 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::::new_witness(cs.clone(), || Ok(field_element)) + }) + .collect::>, 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 crate::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::::new_ref(); + let params = (NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(), 2, 2); + let circuit = NonameFCircuit::::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::>::new_witness(cs.clone(), || Ok(inputs_public.clone())).unwrap(); + let external_inputs_var = + Vec::>::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::::new_ref(); + let params = (NONAME_CIRCUIT_EXTERNAL_INPUTS.to_owned(), 2, 2); + let circuit = NonameFCircuit::::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::>::new_witness(cs.clone(), || Ok(inputs_public)).unwrap(); + let external_inputs_var = + Vec::>::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::::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::>::new_witness(cs.clone(), || Ok(inputs_public)).unwrap(); + + let f_circuit = NonameFCircuit::::new(params).unwrap(); + f_circuit + .generate_step_constraints(cs.clone(), 0, ivc_inputs_var, vec![]) + .unwrap(); + assert!(cs.is_satisfied().unwrap()); + } +} diff --git a/folding-schemes/src/frontend/noname/utils.rs b/folding-schemes/src/frontend/noname/utils.rs new file mode 100644 index 0000000..fdd9281 --- /dev/null +++ b/folding-schemes/src/frontend/noname/utils.rs @@ -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 From<(&Vec, String)> for NonameInputs { + fn from(value: (&Vec, String)) -> Self { + let (values, key) = value; + let mut inputs = HashMap::new(); + if values.is_empty() { + NonameInputs(JsonInputs(inputs)) + } else { + let field_elements: Vec = 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( + value: (&Vec>, String), + ) -> Result { + let (values, key) = value; + let mut inputs = HashMap::new(); + if values.is_empty() { + Ok(NonameInputs(JsonInputs(inputs))) + } else { + let field_elements: Vec = values + .iter() + .map(|var| { + let value = var.value()?; + if value.is_zero() { + Ok("0".to_string()) + } else { + Ok(value.to_string()) + } + }) + .collect::, SynthesisError>>()?; + inputs.insert(key, json!(field_elements)); + Ok(NonameInputs(JsonInputs(inputs))) + } + } +} diff --git a/folding-schemes/src/lib.rs b/folding-schemes/src/lib.rs index fd5280b..4ed0176 100644 --- a/folding-schemes/src/lib.rs +++ b/folding-schemes/src/lib.rs @@ -91,10 +91,12 @@ pub enum Error { NotSupported(String), #[error("max i-th step reached (usize limit reached)")] MaxStep, - #[error("Circom Witness calculation error: {0}")] + #[error("Witness calculation error: {0}")] WitnessCalculationError(String), #[error("BigInt to PrimeField conversion error: {0}")] BigIntConversionError(String), + #[error("Failed to serde: {0}")] + JSONSerdeError(String), } /// FoldingScheme defines trait that is implemented by the diverse folding schemes. It is defined diff --git a/solidity-verifiers/Cargo.toml b/solidity-verifiers/Cargo.toml index f3ad089..0252c14 100644 --- a/solidity-verifiers/Cargo.toml +++ b/solidity-verifiers/Cargo.toml @@ -29,6 +29,7 @@ ark-bn254 = {version="0.4.0", features=["r1cs"]} ark-grumpkin = {version="0.4.0", features=["r1cs"]} rand = "0.8.5" folding-schemes = { path = "../folding-schemes/", features=["light-test"]} +noname = { git = "https://github.com/dmpierre/noname" } [features] default = ["parallel"] @@ -47,3 +48,7 @@ path = "../examples/full_flow.rs" [[example]] name = "circom_full_flow" path = "../examples/circom_full_flow.rs" + +[[example]] +name = "noname_full_flow" +path = "../examples/noname_full_flow.rs"