From 52de2d185c67acf33977bcaf567fb8321191c4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20P=C3=A9rez?= <37264926+CPerezz@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:34:58 +0200 Subject: [PATCH 1/5] feat: Minimal in-browser compatibility for the crate (#149) * change: CircomWrapper constructor to use raw bytes * chore: tmp update to latest circom-compat * feat: Introduce PathOrBin to support in-browser CircomWrapper usage This changes the associated type `Params` of the `CircomFCircuit` to use the newly created `PathOrBin` type. This allows the user of the lib to directly send the binary of the files already read or instead, provide a path to it and let `sonobe` do the work. With this, Circom should be already usable from the browser if we allow JS to take care of reading the `.wasm` and `.r1cs` files. * feat: Introduce PathOrBin to support in-browser NoirFCircuit usage This commit temporarilly stands on top of https://github.com/dmpierre/arkworks_backend/pull/1 referenced as `rev`. This changes the associated type `Params` of the `CircomFCircuit` to use the newly created `PathOrBin` type. This allows the user of the lib to directly send the binary of the files already read or instead, provide a path to it and let `sonobe` do the work. With this, Noir should be already usable from the browser if we allow JS to take care of reading the `circuit.json` files * chore: Update deps to branch instead of `rev` * fix: use PathOrBin in examples * fix: clippy * fix: read file length for initializing vec size --------- Co-authored-by: dmpierre --- examples/circom_full_flow.rs | 2 +- folding-schemes/Cargo.toml | 4 +- folding-schemes/src/frontend/circom/mod.rs | 25 +++++---- folding-schemes/src/frontend/circom/utils.rs | 56 ++++++++++++++------ folding-schemes/src/frontend/noir/mod.rs | 17 +++--- folding-schemes/src/utils/mod.rs | 31 +++++++++++ 6 files changed, 100 insertions(+), 35 deletions(-) diff --git a/examples/circom_full_flow.rs b/examples/circom_full_flow.rs index a78dfa4..8db4a16 100644 --- a/examples/circom_full_flow.rs +++ b/examples/circom_full_flow.rs @@ -61,7 +61,7 @@ fn main() { "./folding-schemes/src/frontend/circom/test_folder/with_external_inputs_js/with_external_inputs.wasm", ); - let f_circuit_params = (r1cs_path, wasm_path, 1, 2); + let f_circuit_params = (r1cs_path.into(), wasm_path.into(), 1, 2); let f_circuit = CircomFCircuit::::new(f_circuit_params).unwrap(); pub type N = diff --git a/folding-schemes/Cargo.toml b/folding-schemes/Cargo.toml index 067b6e1..6fc5bc3 100644 --- a/folding-schemes/Cargo.toml +++ b/folding-schemes/Cargo.toml @@ -25,12 +25,12 @@ num-bigint = "0.4" num-integer = "0.1" color-eyre = "=0.6.2" sha3 = "0.10" -ark-noname = { git = "https://github.com/dmpierre/ark-noname", branch="feat/sonobe-integration" } +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" } +arkworks_backend = { git = "https://github.com/dmpierre/arkworks_backend", branch = "feat/sonobe-integration" } log = "0.4" # tmp import for espresso's sumcheck diff --git a/folding-schemes/src/frontend/circom/mod.rs b/folding-schemes/src/frontend/circom/mod.rs index 303a5de..11d900e 100644 --- a/folding-schemes/src/frontend/circom/mod.rs +++ b/folding-schemes/src/frontend/circom/mod.rs @@ -1,5 +1,6 @@ use crate::frontend::FCircuit; use crate::frontend::FpVar::Var; +use crate::utils::PathOrBin; use crate::Error; use ark_circom::circom::{CircomCircuit, R1CS as CircomR1CS}; use ark_ff::PrimeField; @@ -9,7 +10,6 @@ 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 std::rc::Rc; use std::{fmt, usize}; @@ -93,11 +93,11 @@ impl CircomFCircuit { impl FCircuit for CircomFCircuit { /// (r1cs_path, wasm_path, state_len, external_inputs_len) - type Params = (PathBuf, PathBuf, usize, usize); + type Params = (PathOrBin, PathOrBin, usize, usize); fn new(params: Self::Params) -> Result { let (r1cs_path, wasm_path, state_len, external_inputs_len) = params; - let circom_wrapper = CircomWrapper::new(r1cs_path, wasm_path); + let circom_wrapper = CircomWrapper::new(r1cs_path, wasm_path)?; let r1cs = circom_wrapper.extract_r1cs()?; Ok(Self { @@ -208,6 +208,7 @@ 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] @@ -216,7 +217,8 @@ pub mod tests { let wasm_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - let circom_fcircuit = CircomFCircuit::::new((r1cs_path, wasm_path, 1, 0)).unwrap(); // state_len:1, external_inputs_len:0 + let circom_fcircuit = + CircomFCircuit::::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(); @@ -230,7 +232,8 @@ pub mod tests { let wasm_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - let circom_fcircuit = CircomFCircuit::::new((r1cs_path, wasm_path, 1, 0)).unwrap(); // state_len:1, external_inputs_len:0 + let circom_fcircuit = + CircomFCircuit::::new((r1cs_path.into(), wasm_path.into(), 1, 0)).unwrap(); // state_len:1, external_inputs_len:0 let cs = ConstraintSystem::::new_ref(); @@ -250,7 +253,8 @@ pub mod tests { let wasm_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - let circom_fcircuit = CircomFCircuit::::new((r1cs_path, wasm_path, 1, 0)).unwrap(); // state_len:1, external_inputs_len:0 + let circom_fcircuit = + CircomFCircuit::::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)]; @@ -276,7 +280,8 @@ pub mod tests { let wasm_path = PathBuf::from( "./src/frontend/circom/test_folder/with_external_inputs_js/with_external_inputs.wasm", ); - let circom_fcircuit = CircomFCircuit::::new((r1cs_path, wasm_path, 1, 2)).unwrap(); // state_len:1, external_inputs_len:2 + let circom_fcircuit = + CircomFCircuit::::new((r1cs_path.into(), wasm_path.into(), 1, 2)).unwrap(); // state_len:1, external_inputs_len:2 let cs = ConstraintSystem::::new_ref(); let z_i = vec![Fr::from(3u32)]; let external_inputs = vec![Fr::from(6u32), Fr::from(7u32)]; @@ -319,7 +324,8 @@ pub mod tests { let wasm_path = PathBuf::from( "./src/frontend/circom/test_folder/no_external_inputs_js/no_external_inputs.wasm", ); - let circom_fcircuit = CircomFCircuit::::new((r1cs_path, wasm_path, 3, 0)).unwrap(); + let circom_fcircuit = + CircomFCircuit::::new((r1cs_path.into(), wasm_path.into(), 3, 0)).unwrap(); let cs = ConstraintSystem::::new_ref(); let z_i = vec![Fr::from(3u32), Fr::from(4u32), Fr::from(5u32)]; let z_i_var = Vec::>::new_witness(cs.clone(), || Ok(z_i.clone())).unwrap(); @@ -351,7 +357,8 @@ pub mod tests { let wasm_path = PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); - let mut circom_fcircuit = CircomFCircuit::::new((r1cs_path, wasm_path, 1, 0)).unwrap(); // state_len:1, external_inputs_len:0 + let mut circom_fcircuit = + CircomFCircuit::::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]; diff --git a/folding-schemes/src/frontend/circom/utils.rs b/folding-schemes/src/frontend/circom/utils.rs index 9ee7dfb..f2c8919 100644 --- a/folding-schemes/src/frontend/circom/utils.rs +++ b/folding-schemes/src/frontend/circom/utils.rs @@ -3,40 +3,64 @@ use ark_circom::{ WitnessCalculator, }; use ark_ff::{BigInteger, PrimeField}; +use ark_serialize::Read; use color_eyre::Result; use num_bigint::{BigInt, Sign}; -use std::{fs::File, io::BufReader, marker::PhantomData, path::PathBuf}; +use std::{fs::File, io::Cursor, marker::PhantomData, path::PathBuf}; -use crate::Error; +use crate::{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 { - r1cs_filepath: PathBuf, - wasm_filepath: PathBuf, + r1csfile_bytes: Vec, + wasmfile_bytes: Vec, _marker: PhantomData, } impl CircomWrapper { // 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, + pub fn new(r1cs: PathOrBin, wasm: PathOrBin) -> Result { + 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 { + 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)], ) -> Result<(R1CS, Option>), Error> { // Extracts the R1CS - let file = File::open(&self.r1cs_filepath)?; - let reader = BufReader::new(file); - let r1cs_file = r1cs_reader::R1CSFile::::new(reader)?; + let r1cs_file = r1cs_reader::R1CSFile::::new(Cursor::new(&self.r1csfile_bytes))?; let r1cs = r1cs_reader::R1CS::::from(r1cs_file); // Extracts the witness vector @@ -46,9 +70,7 @@ impl CircomWrapper { } pub fn extract_r1cs(&self) -> Result, Error> { - let file = File::open(&self.r1cs_filepath)?; - let reader = BufReader::new(file); - let r1cs_file = r1cs_reader::R1CSFile::::new(reader)?; + let r1cs_file = r1cs_reader::R1CSFile::::new(Cursor::new(&self.r1csfile_bytes))?; let mut r1cs = r1cs_reader::R1CS::::from(r1cs_file); r1cs.wire_mapping = None; Ok(r1cs) @@ -75,7 +97,7 @@ impl CircomWrapper { &self, inputs: &[(String, Vec)], ) -> Result, Error> { - let mut calculator = WitnessCalculator::new(&self.wasm_filepath).map_err(|e| { + let mut calculator = WitnessCalculator::from_binary(&self.wasmfile_bytes).map_err(|e| { Error::WitnessCalculationError(format!("Failed to create WitnessCalculator: {}", e)) })?; calculator @@ -139,7 +161,7 @@ mod tests { PathBuf::from("./src/frontend/circom/test_folder/cubic_circuit_js/cubic_circuit.wasm"); let inputs = vec![("ivc_input".to_string(), vec![BigInt::from(3)])]; - let wrapper = CircomWrapper::::new(r1cs_path, wasm_path); + let wrapper = CircomWrapper::::new(r1cs_path.into(), wasm_path.into()).unwrap(); let (r1cs, witness) = wrapper.extract_r1cs_and_witness(&inputs).unwrap(); diff --git a/folding-schemes/src/frontend/noir/mod.rs b/folding-schemes/src/frontend/noir/mod.rs index f069c22..6709b20 100644 --- a/folding-schemes/src/frontend/noir/mod.rs +++ b/folding-schemes/src/frontend/noir/mod.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::Error; +use crate::{utils::PathOrBin, Error}; use super::FCircuit; use acvm::{ @@ -16,7 +16,9 @@ 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}; +use arkworks_backend::{ + read_program_from_binary, read_program_from_file, sonobe_bridge::AcirCircuitSonobe, +}; #[derive(Clone, Debug)] pub struct NoirFCircuit { @@ -26,12 +28,15 @@ pub struct NoirFCircuit { } impl FCircuit for NoirFCircuit { - type Params = (String, usize, usize); + type Params = (PathOrBin, 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 (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> = program.functions[0].clone(); let ivc_input_length = circuit.public_parameters.0.len(); let ivc_return_length = circuit.return_values.0.len(); diff --git a/folding-schemes/src/utils/mod.rs b/folding-schemes/src/utils/mod.rs index c2fdc3a..b1bad97 100644 --- a/folding-schemes/src/utils/mod.rs +++ b/folding-schemes/src/utils/mod.rs @@ -1,3 +1,6 @@ +use std::path::Path; +use std::path::PathBuf; + use ark_crypto_primitives::sponge::poseidon::PoseidonConfig; use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::PrimeField; @@ -100,3 +103,31 @@ where &public_params_hash, )) } + +/// Tiny utility enum that allows to import circuits and wasm modules from files by passing their path +/// or passing their content already read. +/// +/// This enum implements the [`From`] trait for both [`Path`], [`PathBuf`] and [`Vec`]. +#[derive(Debug, Clone)] +pub enum PathOrBin { + Path(PathBuf), + Bin(Vec), +} + +impl From<&Path> for PathOrBin { + fn from(value: &Path) -> Self { + PathOrBin::Path(value.into()) + } +} + +impl From for PathOrBin { + fn from(value: PathBuf) -> Self { + PathOrBin::Path(value) + } +} + +impl From> for PathOrBin { + fn from(value: Vec) -> Self { + PathOrBin::Bin(value) + } +} From 1f7bf0462b88252fa5a4783b598a8eed29fcc644 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Tue, 10 Sep 2024 14:38:31 +0200 Subject: [PATCH 2/5] Nova parameters & decider parameters and proofs serialization (#151) * Add nova's decider serialization & deserialization to proof, verifier_params and public inputs * polish * add serialization for nova's ivc proverparams & verifierparams --- folding-schemes/Cargo.toml | 2 +- folding-schemes/src/arith/r1cs.rs | 8 + folding-schemes/src/commitment/ipa.rs | 3 +- folding-schemes/src/commitment/kzg.rs | 2 +- folding-schemes/src/commitment/mod.rs | 4 +- folding-schemes/src/commitment/pedersen.rs | 2 +- .../src/folding/nova/decider_eth.rs | 230 ++++++++++++++++-- folding-schemes/src/folding/nova/mod.rs | 145 ++++++++++- folding-schemes/src/frontend/noir/mod.rs | 2 +- folding-schemes/src/utils/vec.rs | 7 + .../src/verifiers/nova_cyclefold.rs | 35 ++- 11 files changed, 402 insertions(+), 38 deletions(-) diff --git a/folding-schemes/Cargo.toml b/folding-schemes/Cargo.toml index 6fc5bc3..2fbbc3b 100644 --- a/folding-schemes/Cargo.toml +++ b/folding-schemes/Cargo.toml @@ -30,7 +30,7 @@ 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" } +noir_arkworks_backend = { package="arkworks_backend", git = "https://github.com/dmpierre/arkworks_backend", branch = "feat/sonobe-integration" } log = "0.4" # tmp import for espresso's sumcheck diff --git a/folding-schemes/src/arith/r1cs.rs b/folding-schemes/src/arith/r1cs.rs index f6414c7..769daca 100644 --- a/folding-schemes/src/arith/r1cs.rs +++ b/folding-schemes/src/arith/r1cs.rs @@ -44,6 +44,14 @@ impl Arith for R1CS { } impl R1CS { + pub fn empty() -> Self { + R1CS { + l: 0, + A: SparseMatrix::empty(), + B: SparseMatrix::empty(), + C: SparseMatrix::empty(), + } + } pub fn rand(rng: &mut R, n_rows: usize, n_cols: usize) -> Self { Self { l: 1, diff --git a/folding-schemes/src/commitment/ipa.rs b/folding-schemes/src/commitment/ipa.rs index 4607834..a951e7f 100644 --- a/folding-schemes/src/commitment/ipa.rs +++ b/folding-schemes/src/commitment/ipa.rs @@ -18,6 +18,7 @@ use ark_r1cs_std::{ ToBitsGadget, }; use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{cfg_iter, rand::RngCore, UniformRand, Zero}; use core::{borrow::Borrow, marker::PhantomData}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; @@ -30,7 +31,7 @@ use crate::utils::{ }; use crate::Error; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Proof { a: C::ScalarField, l: Vec, diff --git a/folding-schemes/src/commitment/kzg.rs b/folding-schemes/src/commitment/kzg.rs index d034428..5bfce04 100644 --- a/folding-schemes/src/commitment/kzg.rs +++ b/folding-schemes/src/commitment/kzg.rs @@ -70,7 +70,7 @@ impl<'a, C: CurveGroup> Valid for ProverKey<'a, C> { } } -#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Default, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Proof { pub eval: C::ScalarField, pub proof: C, diff --git a/folding-schemes/src/commitment/mod.rs b/folding-schemes/src/commitment/mod.rs index 58ecadd..a76ba15 100644 --- a/folding-schemes/src/commitment/mod.rs +++ b/folding-schemes/src/commitment/mod.rs @@ -13,9 +13,9 @@ pub mod pedersen; /// CommitmentScheme defines the vector commitment scheme trait. Where `H` indicates if to use the /// commitment in hiding mode or not. pub trait CommitmentScheme: Clone + Debug { - type ProverParams: Clone + Debug; + type ProverParams: Clone + Debug + CanonicalSerialize + CanonicalDeserialize; type VerifierParams: Clone + Debug + CanonicalSerialize + CanonicalDeserialize; - type Proof: Clone + Debug; + type Proof: Clone + Debug + CanonicalSerialize + CanonicalDeserialize; type ProverChallenge: Clone + Debug; type Challenge: Clone + Debug; diff --git a/folding-schemes/src/commitment/pedersen.rs b/folding-schemes/src/commitment/pedersen.rs index 4d225ce..0a5f42f 100644 --- a/folding-schemes/src/commitment/pedersen.rs +++ b/folding-schemes/src/commitment/pedersen.rs @@ -12,7 +12,7 @@ use crate::transcript::Transcript; use crate::utils::vec::{vec_add, vec_scalar_mul}; use crate::Error; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Proof { pub R: C, pub u: Vec, diff --git a/folding-schemes/src/folding/nova/decider_eth.rs b/folding-schemes/src/folding/nova/decider_eth.rs index d1a3ed1..5a26813 100644 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ b/folding-schemes/src/folding/nova/decider_eth.rs @@ -5,6 +5,7 @@ use ark_ec::{AffineRepr, CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_groth16::Groth16; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_snark::SNARK; use ark_std::rand::{CryptoRng, RngCore}; use ark_std::{One, Zero}; @@ -22,7 +23,7 @@ use crate::frontend::FCircuit; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Proof where C1: CurveGroup, @@ -40,6 +41,18 @@ where kzg_challenges: [C1::ScalarField; 2], } +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +pub struct VerifierParam +where + C1: CurveGroup, + CS_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, + S_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize, +{ + pub pp_hash: C1::ScalarField, + pub snark_vp: S_VerifyingKey, + pub cs_vp: CS_VerifyingKey, +} + /// Onchain Decider, for ethereum use cases #[derive(Clone, Debug)] pub struct Decider { @@ -90,8 +103,7 @@ where type PreprocessorParam = (FS::ProverParam, FS::VerifierParam); type ProverParam = (S::ProvingKey, CS1::ProverParams); type Proof = Proof; - /// VerifierParam = (pp_hash, snark::vk, commitment_scheme::vk) - type VerifierParam = (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams); + type VerifierParam = VerifierParam; type PublicInput = Vec; type CommittedInstance = CommittedInstance; @@ -122,7 +134,11 @@ where let pp_hash = nova_vp.pp_hash()?; let pp = (g16_pk, nova_pp.cs_pp); - let vp = (pp_hash, g16_vk, nova_vp.cs_vp); + let vp = Self::VerifierParam { + pp_hash, + snark_vp: g16_vk, + cs_vp: nova_vp.cs_vp, + }; Ok((pp, vp)) } @@ -191,9 +207,6 @@ where return Err(Error::NotEnoughSteps); } - let (pp_hash, snark_vk, cs_vk): (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams) = - vp; - // compute U = U_{d+1}= NIFS.V(U_d, u_d, cmT) let U = NIFS::::verify(proof.r, running_instance, incoming_instance, &proof.cmT); @@ -202,7 +215,7 @@ where let (cmT_x, cmT_y) = NonNativeAffineVar::inputize(proof.cmT)?; let public_input: Vec = [ - vec![pp_hash, i], + vec![vp.pp_hash, i], z_0, z_i, vec![U.u], @@ -222,7 +235,7 @@ where ] .concat(); - let snark_v = S::verify(&snark_vk, &public_input, &proof.snark_proof) + let snark_v = S::verify(&vp.snark_vp, &public_input, &proof.snark_proof) .map_err(|e| Error::Other(e.to_string()))?; if !snark_v { return Err(Error::SNARKVerificationFail); @@ -230,13 +243,13 @@ where // we're at the Ethereum EVM case, so the CS1 is KZG commitments CS1::verify_with_challenge( - &cs_vk, + &vp.cs_vp, proof.kzg_challenges[0], &U.cmW, &proof.kzg_proofs[0], )?; CS1::verify_with_challenge( - &cs_vk, + &vp.cs_vp, proof.kzg_challenges[1], &U.cmE, &proof.kzg_proofs[1], @@ -326,7 +339,9 @@ pub mod tests { use super::*; use crate::commitment::pedersen::Pedersen; - use crate::folding::nova::PreprocessorParam; + use crate::folding::nova::{ + PreprocessorParam, ProverParams as NovaProverParams, VerifierParams as NovaVerifierParams, + }; use crate::frontend::tests::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; @@ -355,28 +370,140 @@ pub mod tests { N, // here we define the FoldingScheme to use >; - let mut rng = ark_std::test_rng(); + let mut rng = rand::rngs::OsRng; let poseidon_config = poseidon_canonical_config::(); let F_circuit = CubicFCircuit::::new(()).unwrap(); let z_0 = vec![Fr::from(3_u32)]; - let prep_param = PreprocessorParam::new(poseidon_config, F_circuit); - let nova_params = N::preprocess(&mut rng, &prep_param).unwrap(); + let preprocessor_param = PreprocessorParam::new(poseidon_config, F_circuit); + let nova_params = N::preprocess(&mut rng, &preprocessor_param).unwrap(); let start = Instant::now(); let mut nova = N::init(&nova_params, F_circuit, z_0.clone()).unwrap(); println!("Nova initialized, {:?}", start.elapsed()); + + // prepare the Decider prover & verifier params + let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); + let start = Instant::now(); nova.prove_step(&mut rng, vec![], None).unwrap(); println!("prove_step, {:?}", start.elapsed()); nova.prove_step(&mut rng, vec![], None).unwrap(); // do a 2nd step + // decider proof generation + let start = Instant::now(); + let proof = D::prove(rng, decider_pp, nova.clone()).unwrap(); + println!("Decider prove, {:?}", start.elapsed()); + + // decider proof verification + let start = Instant::now(); + let verified = D::verify( + decider_vp.clone(), + nova.i.clone(), + nova.z_0.clone(), + nova.z_i.clone(), + &nova.U_i, + &nova.u_i, + &proof, + ) + .unwrap(); + assert!(verified); + println!("Decider verify, {:?}", start.elapsed()); + + // decider proof verification using the deserialized data + let verified = D::verify( + decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, &proof, + ) + .unwrap(); + assert!(verified); + } + + // Test to check the serialization and deserialization of diverse Decider related parameters. + // This test is the same test as `test_decider` but it serializes values and then uses the + // deserialized values to continue the checks. + #[test] + fn test_decider_serialization() { + // use Nova as FoldingScheme + type N = Nova< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + KZG<'static, Bn254>, + Pedersen, + false, + >; + type D = Decider< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + KZG<'static, Bn254>, + Pedersen, + Groth16, // here we define the Snark to use in the decider + N, // here we define the FoldingScheme to use + >; + let mut rng = rand::rngs::OsRng; + let poseidon_config = poseidon_canonical_config::(); + + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let z_0 = vec![Fr::from(3_u32)]; + + let preprocessor_param = PreprocessorParam::new(poseidon_config, F_circuit); + let nova_params = N::preprocess(&mut rng, &preprocessor_param).unwrap(); + + let start = Instant::now(); + let nova = N::init(&nova_params, F_circuit, z_0.clone()).unwrap(); + println!("Nova initialized, {:?}", start.elapsed()); // prepare the Decider prover & verifier params let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); + // serialize the Nova params. These params are the trusted setup of the commitment schemes used + // (ie. KZG & Pedersen in this case) + let mut nova_pp_serialized = vec![]; + nova_params + .0 + .serialize_compressed(&mut nova_pp_serialized) + .unwrap(); + let mut nova_vp_serialized = vec![]; + nova_params + .1 + .serialize_compressed(&mut nova_vp_serialized) + .unwrap(); + // deserialize the Nova params. This would be done by the client reading from a file + let nova_pp_deserialized = NovaProverParams::< + Projective, + Projective2, + KZG<'static, Bn254>, + Pedersen, + >::deserialize_compressed( + &mut nova_pp_serialized.as_slice() + ) + .unwrap(); + let nova_vp_deserialized = NovaVerifierParams::< + Projective, + Projective2, + KZG<'static, Bn254>, + Pedersen, + >::deserialize_compressed( + &mut nova_vp_serialized.as_slice() + ) + .unwrap(); + + // initialize nova again, but from the deserialized parameters + let nova_params = (nova_pp_deserialized, nova_vp_deserialized); + let mut nova = N::init(&nova_params, F_circuit, z_0).unwrap(); + + let start = Instant::now(); + nova.prove_step(&mut rng, vec![], None).unwrap(); + println!("prove_step, {:?}", start.elapsed()); + nova.prove_step(&mut rng, vec![], None).unwrap(); // do a 2nd step + // decider proof generation let start = Instant::now(); let proof = D::prove(rng, decider_pp, nova.clone()).unwrap(); @@ -385,10 +512,81 @@ pub mod tests { // decider proof verification let start = Instant::now(); let verified = D::verify( - decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, &proof, + decider_vp.clone(), + nova.i.clone(), + nova.z_0.clone(), + nova.z_i.clone(), + &nova.U_i, + &nova.u_i, + &proof, ) .unwrap(); assert!(verified); println!("Decider verify, {:?}", start.elapsed()); + + // The rest of this test will serialize the data and deserialize it back, and use it to + // verify the proof: + + // serialize the verifier_params, proof and public inputs + let mut decider_vp_serialized = vec![]; + decider_vp + .serialize_compressed(&mut decider_vp_serialized) + .unwrap(); + let mut proof_serialized = vec![]; + proof.serialize_compressed(&mut proof_serialized).unwrap(); + // serialize the public inputs in a single packet + let mut public_inputs_serialized = vec![]; + nova.i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + nova.z_0 + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + nova.z_i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + nova.U_i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + nova.u_i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + + // deserialize back the verifier_params, proof and public inputs + let decider_vp_deserialized = + VerifierParam::< + Projective, + as CommitmentScheme>::VerifierParams, + as SNARK>::VerifyingKey, + >::deserialize_compressed(&mut decider_vp_serialized.as_slice()) + .unwrap(); + let proof_deserialized = + Proof::, Groth16>::deserialize_compressed( + &mut proof_serialized.as_slice(), + ) + .unwrap(); + + // deserialize the public inputs from the single packet 'public_inputs_serialized' + let mut reader = public_inputs_serialized.as_slice(); + let i_deserialized = Fr::deserialize_compressed(&mut reader).unwrap(); + let z_0_deserialized = Vec::::deserialize_compressed(&mut reader).unwrap(); + let z_i_deserialized = Vec::::deserialize_compressed(&mut reader).unwrap(); + let U_i_deserialized = + CommittedInstance::::deserialize_compressed(&mut reader).unwrap(); + let u_i_deserialized = + CommittedInstance::::deserialize_compressed(&mut reader).unwrap(); + + // decider proof verification using the deserialized data + let verified = D::verify( + decider_vp_deserialized, + i_deserialized, + z_0_deserialized, + z_i_deserialized, + &U_i_deserialized, + &u_i_deserialized, + &proof_deserialized, + ) + .unwrap(); + assert!(verified); } } diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs index f0c07b1..be57fc9 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -8,7 +8,7 @@ use ark_ec::{CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Valid}; use ark_std::fmt::Debug; use ark_std::rand::RngCore; use ark_std::{One, UniformRand, Zero}; @@ -21,7 +21,7 @@ use crate::folding::circuits::cyclefold::{ }; use crate::folding::circuits::CF2; use crate::frontend::FCircuit; -use crate::transcript::{AbsorbNonNative, Transcript}; +use crate::transcript::{poseidon::poseidon_canonical_config, AbsorbNonNative, Transcript}; use crate::utils::vec::is_zero_vec; use crate::Error; use crate::FoldingScheme; @@ -190,7 +190,7 @@ where } #[derive(Debug, Clone)] -pub struct PreprocessorParam +pub struct PreprocessorParam where C1: CurveGroup, C2: CurveGroup, @@ -228,7 +228,7 @@ where } #[derive(Debug, Clone)] -pub struct ProverParams +pub struct ProverParams where C1: CurveGroup, C2: CurveGroup, @@ -240,8 +240,70 @@ where pub cf_cs_pp: CS2::ProverParams, } +impl Valid for ProverParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + fn check(&self) -> Result<(), ark_serialize::SerializationError> { + self.poseidon_config.full_rounds.check()?; + self.poseidon_config.partial_rounds.check()?; + self.poseidon_config.alpha.check()?; + self.poseidon_config.ark.check()?; + self.poseidon_config.mds.check()?; + self.poseidon_config.rate.check()?; + self.poseidon_config.capacity.check()?; + self.cs_pp.check()?; + self.cf_cs_pp.check()?; + Ok(()) + } +} +impl CanonicalSerialize for ProverParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + fn serialize_with_mode( + &self, + mut writer: W, + compress: ark_serialize::Compress, + ) -> Result<(), ark_serialize::SerializationError> { + self.cs_pp.serialize_with_mode(&mut writer, compress)?; + self.cf_cs_pp.serialize_with_mode(&mut writer, compress) + } + + fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { + self.cs_pp.serialized_size(compress) + self.cf_cs_pp.serialized_size(compress) + } +} +impl CanonicalDeserialize for ProverParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + fn deserialize_with_mode( + mut reader: R, + compress: ark_serialize::Compress, + validate: ark_serialize::Validate, + ) -> Result { + let cs_pp = CS1::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; + let cf_cs_pp = CS2::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; + Ok(ProverParams { + poseidon_config: poseidon_canonical_config::(), + cs_pp, + cf_cs_pp, + }) + } +} + #[derive(Debug, Clone)] -pub struct VerifierParams +pub struct VerifierParams where C1: CurveGroup, C2: CurveGroup, @@ -255,6 +317,79 @@ where pub cf_cs_vp: CS2::VerifierParams, } +impl Valid for VerifierParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + fn check(&self) -> Result<(), ark_serialize::SerializationError> { + self.poseidon_config.full_rounds.check()?; + self.poseidon_config.partial_rounds.check()?; + self.poseidon_config.alpha.check()?; + self.poseidon_config.ark.check()?; + self.poseidon_config.mds.check()?; + self.poseidon_config.rate.check()?; + self.poseidon_config.capacity.check()?; + self.r1cs.check()?; + self.cf_r1cs.check()?; + self.cs_vp.check()?; + self.cf_cs_vp.check()?; + Ok(()) + } +} +impl CanonicalSerialize for VerifierParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + fn serialize_with_mode( + &self, + mut writer: W, + compress: ark_serialize::Compress, + ) -> Result<(), ark_serialize::SerializationError> { + self.r1cs.serialize_with_mode(&mut writer, compress)?; + self.cf_r1cs.serialize_with_mode(&mut writer, compress)?; + self.cs_vp.serialize_with_mode(&mut writer, compress)?; + self.cf_cs_vp.serialize_with_mode(&mut writer, compress) + } + + fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { + self.r1cs.serialized_size(compress) + + self.cf_r1cs.serialized_size(compress) + + self.cs_vp.serialized_size(compress) + + self.cf_cs_vp.serialized_size(compress) + } +} +impl CanonicalDeserialize for VerifierParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + fn deserialize_with_mode( + mut reader: R, + compress: ark_serialize::Compress, + validate: ark_serialize::Validate, + ) -> Result { + let r1cs = R1CS::deserialize_with_mode(&mut reader, compress, validate)?; + let cf_r1cs = R1CS::deserialize_with_mode(&mut reader, compress, validate)?; + let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; + let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; + Ok(VerifierParams { + poseidon_config: poseidon_canonical_config::(), + r1cs, + cf_r1cs, + cs_vp, + cf_cs_vp, + }) + } +} + impl VerifierParams where C1: CurveGroup, diff --git a/folding-schemes/src/frontend/noir/mod.rs b/folding-schemes/src/frontend/noir/mod.rs index 6709b20..b091091 100644 --- a/folding-schemes/src/frontend/noir/mod.rs +++ b/folding-schemes/src/frontend/noir/mod.rs @@ -16,7 +16,7 @@ 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::{ +use noir_arkworks_backend::{ read_program_from_binary, read_program_from_file, sonobe_bridge::AcirCircuitSonobe, }; diff --git a/folding-schemes/src/utils/vec.rs b/folding-schemes/src/utils/vec.rs index 93c4b66..3ac9203 100644 --- a/folding-schemes/src/utils/vec.rs +++ b/folding-schemes/src/utils/vec.rs @@ -20,6 +20,13 @@ pub struct SparseMatrix { } impl SparseMatrix { + pub fn empty() -> Self { + Self { + n_rows: 0, + n_cols: 0, + coeffs: vec![], + } + } pub fn rand(rng: &mut R, n_rows: usize, n_cols: usize) -> Self { const ZERO_VAL_PROBABILITY: f64 = 0.8f64; diff --git a/solidity-verifiers/src/verifiers/nova_cyclefold.rs b/solidity-verifiers/src/verifiers/nova_cyclefold.rs index 391e485..9e896bd 100644 --- a/solidity-verifiers/src/verifiers/nova_cyclefold.rs +++ b/solidity-verifiers/src/verifiers/nova_cyclefold.rs @@ -2,13 +2,14 @@ #![allow(non_camel_case_types)] #![allow(clippy::upper_case_acronyms)] -use ark_bn254::{Bn254, Fq, Fr, G1Affine}; +use ark_bn254::{Bn254, Fq, Fr, G1Affine, G1Projective}; use ark_groth16::VerifyingKey as ArkG16VerifierKey; use ark_poly_commit::kzg10::VerifierKey as ArkKZG10VerifierKey; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use askama::Template; use folding_schemes::folding::circuits::nonnative::uint::NonNativeUintVar; +use folding_schemes::folding::nova::decider_eth::VerifierParam as DeciderVerifierParam; use super::g16::Groth16Verifier; use super::kzg::KZG10Verifier; @@ -92,22 +93,26 @@ impl From<(Fr, Groth16VerifierKey, KZG10VerifierKey, usize)> for NovaCycleFoldVe // in the NovaCycleFoldDecider verifier contract impl From<( - (Fr, ArkG16VerifierKey, ArkKZG10VerifierKey), + DeciderVerifierParam, ArkG16VerifierKey>, usize, )> for NovaCycleFoldVerifierKey { fn from( value: ( - (Fr, ArkG16VerifierKey, ArkKZG10VerifierKey), + DeciderVerifierParam< + G1Projective, + ArkKZG10VerifierKey, + ArkG16VerifierKey, + >, usize, ), ) -> Self { let decider_vp = value.0; - let g16_vk = Groth16VerifierKey::from(decider_vp.1); + let g16_vk = Groth16VerifierKey::from(decider_vp.snark_vp); // pass `Vec::new()` since batchCheck will not be used - let kzg_vk = KZG10VerifierKey::from((decider_vp.2, Vec::new())); + let kzg_vk = KZG10VerifierKey::from((decider_vp.cs_vp, Vec::new())); Self { - pp_hash: decider_vp.0, + pp_hash: decider_vp.pp_hash, g16_vk, kzg_vk, z_len: value.1, @@ -157,7 +162,7 @@ mod tests { Decider, Error, FoldingScheme, }; - use super::NovaCycleFoldDecider; + use super::{DeciderVerifierParam, NovaCycleFoldDecider}; use crate::verifiers::tests::{setup, DEFAULT_SETUP_LEN}; use crate::{ evm::{compile_solidity, save_solidity, Evm}, @@ -286,9 +291,14 @@ mod tests { fn nova_cyclefold_vk_serde_roundtrip() { let (pp_hash, _, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); - let mut bytes = vec![]; - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from(((pp_hash, g16_vk, kzg_vk), 1)); + let decider_vp = DeciderVerifierParam { + pp_hash, + snark_vp: g16_vk, + cs_vp: kzg_vk, + }; + let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, 1)); + let mut bytes = vec![]; nova_cyclefold_vk .serialize_protocol_verifier_key(&mut bytes) .unwrap(); @@ -301,7 +311,12 @@ mod tests { #[test] fn nova_cyclefold_decider_template_renders() { let (pp_hash, _, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN); - let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from(((pp_hash, g16_vk, kzg_vk), 1)); + let decider_vp = DeciderVerifierParam { + pp_hash, + snark_vp: g16_vk, + cs_vp: kzg_vk, + }; + let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((decider_vp, 1)); let decider_solidity_code = HeaderInclusion::::builder() .template(nova_cyclefold_vk) From 0ad54576ec6462d63313ebde18bb0a24669fd00e Mon Sep 17 00:00:00 2001 From: arnaucube Date: Tue, 10 Sep 2024 17:10:00 +0200 Subject: [PATCH 3/5] implement HyperNova's DeciderEth (#156) * implement HyperNova's DeciderEth * add remark about Nova's zk layer implementation and the 3 identified use cases --- examples/circom_full_flow.rs | 2 +- examples/full_flow.rs | 2 +- examples/noir_full_flow.rs | 2 +- examples/noname_full_flow.rs | 2 +- .../src/folding/hypernova/decider_eth.rs | 312 ++++++++++++++++++ .../folding/hypernova/decider_eth_circuit.rs | 2 +- folding-schemes/src/folding/hypernova/mod.rs | 25 +- .../src/folding/hypernova/nimfs.rs | 4 +- .../src/folding/nova/decider_eth.rs | 11 +- folding-schemes/src/folding/nova/zk.rs | 33 +- folding-schemes/src/lib.rs | 2 +- .../src/verifiers/nova_cyclefold.rs | 2 +- 12 files changed, 372 insertions(+), 27 deletions(-) create mode 100644 folding-schemes/src/folding/hypernova/decider_eth.rs diff --git a/examples/circom_full_flow.rs b/examples/circom_full_flow.rs index 8db4a16..10a79ba 100644 --- a/examples/circom_full_flow.rs +++ b/examples/circom_full_flow.rs @@ -89,7 +89,7 @@ fn main() { 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(); + 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() { diff --git a/examples/full_flow.rs b/examples/full_flow.rs index 5c9ce18..bf1f28f 100644 --- a/examples/full_flow.rs +++ b/examples/full_flow.rs @@ -106,7 +106,7 @@ fn main() { let mut nova = N::init(&nova_params, f_circuit, z_0).unwrap(); // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); + 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..n_steps { diff --git a/examples/noir_full_flow.rs b/examples/noir_full_flow.rs index 1d5ba52..ec5107a 100644 --- a/examples/noir_full_flow.rs +++ b/examples/noir_full_flow.rs @@ -79,7 +79,7 @@ fn main() { 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(); + 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 { diff --git a/examples/noname_full_flow.rs b/examples/noname_full_flow.rs index a095475..73a1a67 100644 --- a/examples/noname_full_flow.rs +++ b/examples/noname_full_flow.rs @@ -89,7 +89,7 @@ fn main() { 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(); + 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() { diff --git a/folding-schemes/src/folding/hypernova/decider_eth.rs b/folding-schemes/src/folding/hypernova/decider_eth.rs new file mode 100644 index 0000000..381a6b3 --- /dev/null +++ b/folding-schemes/src/folding/hypernova/decider_eth.rs @@ -0,0 +1,312 @@ +/// This file implements the HyperNova's onchain (Ethereum's EVM) decider. +use ark_crypto_primitives::sponge::Absorb; +use ark_ec::{CurveGroup, Group}; +use ark_ff::PrimeField; +use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_snark::SNARK; +use ark_std::rand::{CryptoRng, RngCore}; +use ark_std::{One, Zero}; +use core::marker::PhantomData; + +pub use super::decider_eth_circuit::DeciderEthCircuit; +use super::{lcccs::LCCCS, HyperNova}; +use crate::commitment::{ + kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme, +}; +use crate::folding::circuits::{nonnative::affine::NonNativeAffineVar, CF2}; +use crate::frontend::FCircuit; +use crate::Error; +use crate::{Decider as DeciderTrait, FoldingScheme}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Proof +where + C1: CurveGroup, + CS1: CommitmentScheme, + S: SNARK, +{ + snark_proof: S::Proof, + kzg_proof: CS1::Proof, + // rho used at the last fold, U_{i+1}=NIMFS.V(rho, U_i, u_i), it is checked in-circuit + rho: C1::ScalarField, + U_i1: LCCCS, // U_{i+1}, which is checked in-circuit + // the KZG challenge is provided by the prover, but in-circuit it is checked to match + // the in-circuit computed computed one. + kzg_challenge: C1::ScalarField, +} + +/// Onchain Decider, for ethereum use cases +#[derive(Clone, Debug)] +pub struct Decider { + _c1: PhantomData, + _gc1: PhantomData, + _c2: PhantomData, + _gc2: PhantomData, + _fc: PhantomData, + _cs1: PhantomData, + _cs2: PhantomData, + _s: PhantomData, + _fs: PhantomData, +} + +impl + DeciderTrait for Decider +where + C1: CurveGroup, + C2: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + // CS1 is a KZG commitment, where challenge is C1::Fr elem + CS1: CommitmentScheme< + C1, + ProverChallenge = C1::ScalarField, + Challenge = C1::ScalarField, + Proof = KZGProof, + >, + // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider + CS2: CommitmentScheme>, + S: SNARK, + FS: FoldingScheme, + ::BaseField: PrimeField, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + ::ScalarField: Absorb, + C1: CurveGroup, + for<'b> &'b GC1: GroupOpsBounds<'b, C1, GC1>, + for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, + // constrain FS into HyperNova, since this is a Decider specifically for HyperNova + HyperNova: From, + crate::folding::hypernova::ProverParams: + From<>::ProverParam>, + crate::folding::hypernova::VerifierParams: + From<>::VerifierParam>, +{ + type PreprocessorParam = (FS::ProverParam, FS::VerifierParam); + type ProverParam = (S::ProvingKey, CS1::ProverParams); + type Proof = Proof; + /// VerifierParam = (pp_hash, snark::vk, commitment_scheme::vk) + type VerifierParam = (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams); + type PublicInput = Vec; + type CommittedInstance = (); + + fn preprocess( + mut rng: impl RngCore + CryptoRng, + prep_param: Self::PreprocessorParam, + fs: FS, + ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { + let circuit = + DeciderEthCircuit::::from_hypernova::( + fs.into(), + ) + .unwrap(); + + // get the Groth16 specific setup for the circuit + let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng).unwrap(); + + // get the FoldingScheme prover & verifier params from HyperNova + #[allow(clippy::type_complexity)] + let hypernova_pp: as FoldingScheme< + C1, + C2, + FC, + >>::ProverParam = prep_param.0.into(); + #[allow(clippy::type_complexity)] + let hypernova_vp: as FoldingScheme< + C1, + C2, + FC, + >>::VerifierParam = prep_param.1.into(); + let pp_hash = hypernova_vp.pp_hash()?; + + let pp = (g16_pk, hypernova_pp.cs_pp); + let vp = (pp_hash, g16_vk, hypernova_vp.cs_vp); + Ok((pp, vp)) + } + + fn prove( + mut rng: impl RngCore + CryptoRng, + pp: Self::ProverParam, + folding_scheme: FS, + ) -> Result { + let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; + + let circuit = DeciderEthCircuit::::from_hypernova::( + folding_scheme.into(), + )?; + + let snark_proof = S::prove(&snark_pk, circuit.clone(), &mut rng) + .map_err(|e| Error::Other(e.to_string()))?; + + // Notice that since the `circuit` has been constructed at the `from_hypernova` call, which + // in case of failure it would have returned an error there, the next two unwraps should + // never reach an error. + let rho_Fr = circuit.rho.ok_or(Error::Empty)?; + let W_i1 = circuit.W_i1.ok_or(Error::Empty)?; + + // get the challenges that have been already computed when preparing the circuit inputs in + // the above `from_hypernova` call + let challenge_W = circuit + .kzg_challenge + .ok_or(Error::MissingValue("kzg_challenge".to_string()))?; + + // generate KZG proofs + let U_cmW_proof = CS1::prove_with_challenge( + &cs_pk, + challenge_W, + &W_i1.w, + &C1::ScalarField::zero(), + None, + )?; + + Ok(Self::Proof { + snark_proof, + kzg_proof: U_cmW_proof, + rho: rho_Fr, + U_i1: circuit.U_i1.ok_or(Error::Empty)?, + kzg_challenge: challenge_W, + }) + } + + fn verify( + vp: Self::VerifierParam, + i: C1::ScalarField, + z_0: Vec, + z_i: Vec, + // we don't use the instances at the verifier level, since we check them in-circuit + _running_instance: &Self::CommittedInstance, + _incoming_instance: &Self::CommittedInstance, + proof: &Self::Proof, + ) -> Result { + if i <= C1::ScalarField::one() { + return Err(Error::NotEnoughSteps); + } + + let (pp_hash, snark_vk, cs_vk): (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams) = + vp; + + // Note: the NIMFS proof is checked inside the DeciderEthCircuit, which ensures that the + // 'proof.U_i1' is correctly computed + + let (cmC_x, cmC_y) = NonNativeAffineVar::inputize(proof.U_i1.C)?; + + let public_input: Vec = [ + vec![pp_hash, i], + z_0, + z_i, + // U_i+1: + cmC_x, + cmC_y, + vec![proof.U_i1.u], + proof.U_i1.x.clone(), + proof.U_i1.r_x.clone(), + proof.U_i1.v.clone(), + vec![proof.kzg_challenge, proof.kzg_proof.eval, proof.rho], + ] + .concat(); + + let snark_v = S::verify(&snark_vk, &public_input, &proof.snark_proof) + .map_err(|e| Error::Other(e.to_string()))?; + if !snark_v { + return Err(Error::SNARKVerificationFail); + } + + // we're at the Ethereum EVM case, so the CS1 is KZG commitments + CS1::verify_with_challenge(&cs_vk, proof.kzg_challenge, &proof.U_i1.C, &proof.kzg_proof)?; + + Ok(true) + } +} + +#[cfg(test)] +pub mod tests { + use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective}; + use ark_groth16::Groth16; + use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; + use std::time::Instant; + + use super::*; + use crate::commitment::{kzg::KZG, pedersen::Pedersen}; + use crate::folding::hypernova::PreprocessorParam; + use crate::frontend::tests::CubicFCircuit; + use crate::transcript::poseidon::poseidon_canonical_config; + + #[test] + fn test_decider() { + const MU: usize = 1; + const NU: usize = 1; + // use HyperNova as FoldingScheme + type HN = HyperNova< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + KZG<'static, Bn254>, + Pedersen, + MU, + NU, + false, + >; + type D = Decider< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + KZG<'static, Bn254>, + Pedersen, + Groth16, // here we define the Snark to use in the decider + HN, // here we define the FoldingScheme to use + MU, + NU, + >; + + let mut rng = ark_std::test_rng(); + let poseidon_config = poseidon_canonical_config::(); + + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let z_0 = vec![Fr::from(3_u32)]; + + let prep_param = PreprocessorParam::new(poseidon_config, F_circuit); + let hypernova_params = HN::preprocess(&mut rng, &prep_param).unwrap(); + + let start = Instant::now(); + let mut hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone()).unwrap(); + println!("Nova initialized, {:?}", start.elapsed()); + let start = Instant::now(); + hypernova + .prove_step(&mut rng, vec![], Some((vec![], vec![]))) + .unwrap(); + println!("prove_step, {:?}", start.elapsed()); + hypernova + .prove_step(&mut rng, vec![], Some((vec![], vec![]))) + .unwrap(); // do a 2nd step + + let mut rng = rand::rngs::OsRng; + + // prepare the Decider prover & verifier params + let (decider_pp, decider_vp) = + D::preprocess(&mut rng, hypernova_params, hypernova.clone()).unwrap(); + + // decider proof generation + let start = Instant::now(); + let proof = D::prove(rng, decider_pp, hypernova.clone()).unwrap(); + println!("Decider prove, {:?}", start.elapsed()); + + // decider proof verification + let start = Instant::now(); + let verified = D::verify( + decider_vp, + hypernova.i, + hypernova.z_0, + hypernova.z_i, + &(), + &(), + &proof, + ) + .unwrap(); + assert!(verified); + println!("Decider verify, {:?}", start.elapsed()); + } +} diff --git a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs index ac95cbb..ec8c650 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs @@ -231,7 +231,7 @@ where cf_E_len: hn.cf_W_i.E.len(), ccs: hn.ccs, cf_r1cs: hn.cf_r1cs, - cf_pedersen_params: hn.cf_cs_params, + cf_pedersen_params: hn.cf_cs_pp, poseidon_config: hn.poseidon_config, pp_hash: Some(hn.pp_hash), i: Some(hn.i), diff --git a/folding-schemes/src/folding/hypernova/mod.rs b/folding-schemes/src/folding/hypernova/mod.rs index c2656a0..fe75a9b 100644 --- a/folding-schemes/src/folding/hypernova/mod.rs +++ b/folding-schemes/src/folding/hypernova/mod.rs @@ -10,6 +10,7 @@ use ark_std::{fmt::Debug, marker::PhantomData, rand::RngCore, One, Zero}; pub mod cccs; pub mod circuits; +pub mod decider_eth; pub mod decider_eth_circuit; pub mod lcccs; pub mod nimfs; @@ -84,8 +85,8 @@ where CS2: CommitmentScheme, { pub poseidon_config: PoseidonConfig, - pub cs_params: CS1::ProverParams, - pub cf_cs_params: CS2::ProverParams, + pub cs_pp: CS1::ProverParams, + pub cf_cs_pp: CS2::ProverParams, // if ccs is set, it will be used, if not, it will be computed at runtime pub ccs: Option>, } @@ -162,9 +163,9 @@ pub struct HyperNova< pub cf_r1cs: R1CS, pub poseidon_config: PoseidonConfig, /// CommitmentScheme::ProverParams over C1 - pub cs_params: CS1::ProverParams, + pub cs_pp: CS1::ProverParams, /// CycleFold CommitmentScheme::ProverParams, over C2 - pub cf_cs_params: CS2::ProverParams, + pub cf_cs_pp: CS2::ProverParams, /// F circuit, the circuit that is being folded pub F: FC, /// public params hash @@ -221,7 +222,7 @@ where // assign them directly to w_i, u_i. let (U_i, W_i) = self .ccs - .to_lcccs::<_, _, CS1, H>(&mut rng, &self.cs_params, &r1cs_z)?; + .to_lcccs::<_, _, CS1, H>(&mut rng, &self.cs_pp, &r1cs_z)?; #[cfg(test)] U_i.check_relation(&self.ccs, &W_i)?; @@ -243,7 +244,7 @@ where // assign them directly to w_i, u_i. let (u_i, w_i) = self .ccs - .to_cccs::<_, _, CS1, H>(&mut rng, &self.cs_params, &r1cs_z)?; + .to_cccs::<_, _, CS1, H>(&mut rng, &self.cs_pp, &r1cs_z)?; #[cfg(test)] u_i.check_relation(&self.ccs, &w_i)?; @@ -426,8 +427,8 @@ where let pp = ProverParams:: { poseidon_config: prep_param.poseidon_config.clone(), - cs_params: cs_pp.clone(), - cf_cs_params: cf_cs_pp.clone(), + cs_pp, + cf_cs_pp, ccs: Some(ccs.clone()), }; let vp = VerifierParams:: { @@ -496,8 +497,8 @@ where ccs, cf_r1cs, poseidon_config: pp.poseidon_config.clone(), - cs_params: pp.cs_params.clone(), - cf_cs_params: pp.cf_cs_params.clone(), + cs_pp: pp.cs_pp.clone(), + cf_cs_pp: pp.cf_cs_pp.clone(), F, pp_hash, i: C1::ScalarField::zero(), @@ -736,7 +737,7 @@ where >( &mut transcript_p, self.cf_r1cs.clone(), - self.cf_cs_params.clone(), + self.cf_cs_pp.clone(), self.pp_hash, self.cf_W_i.clone(), // CycleFold running instance witness self.cf_U_i.clone(), // CycleFold running instance @@ -796,7 +797,7 @@ where // assign them directly to w_i, u_i. let (u_i, w_i) = self .ccs - .to_cccs::<_, C1, CS1, H>(&mut rng, &self.cs_params, &r1cs_z)?; + .to_cccs::<_, C1, CS1, H>(&mut rng, &self.cs_pp, &r1cs_z)?; self.u_i = u_i.clone(); self.w_i = w_i.clone(); diff --git a/folding-schemes/src/folding/hypernova/nimfs.rs b/folding-schemes/src/folding/hypernova/nimfs.rs index 2a70b28..6312162 100644 --- a/folding-schemes/src/folding/hypernova/nimfs.rs +++ b/folding-schemes/src/folding/hypernova/nimfs.rs @@ -23,7 +23,7 @@ use std::fmt::Debug; use std::marker::PhantomData; /// NIMFSProof defines a multifolding proof -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct NIMFSProof { pub sc_proof: SumCheckProof, pub sigmas_thetas: SigmasThetas, @@ -51,7 +51,7 @@ impl NIMFSProof { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct SigmasThetas(pub Vec>, pub Vec>); #[derive(Debug)] diff --git a/folding-schemes/src/folding/nova/decider_eth.rs b/folding-schemes/src/folding/nova/decider_eth.rs index 5a26813..6377b31 100644 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ b/folding-schemes/src/folding/nova/decider_eth.rs @@ -1,4 +1,4 @@ -/// This file implements the onchain (Ethereum's EVM) decider. +/// This file implements the Nova's onchain (Ethereum's EVM) decider. use ark_bn254::Bn254; use ark_crypto_primitives::sponge::Absorb; use ark_ec::{AffineRepr, CurveGroup, Group}; @@ -11,7 +11,7 @@ use ark_std::rand::{CryptoRng, RngCore}; use ark_std::{One, Zero}; use core::marker::PhantomData; -pub use super::decider_eth_circuit::{DeciderEthCircuit, KZGChallengesGadget}; +pub use super::decider_eth_circuit::DeciderEthCircuit; use super::{nifs::NIFS, CommittedInstance, Nova}; use crate::commitment::{ kzg::{Proof as KZGProof, KZG}, @@ -109,7 +109,7 @@ where fn preprocess( mut rng: impl RngCore + CryptoRng, - prep_param: &Self::PreprocessorParam, + prep_param: Self::PreprocessorParam, fs: FS, ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { let circuit = @@ -384,7 +384,7 @@ pub mod tests { println!("Nova initialized, {:?}", start.elapsed()); // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); + let (decider_pp, decider_vp) = D::preprocess(&mut rng, nova_params, nova.clone()).unwrap(); let start = Instant::now(); nova.prove_step(&mut rng, vec![], None).unwrap(); @@ -461,7 +461,8 @@ pub mod tests { println!("Nova initialized, {:?}", start.elapsed()); // prepare the Decider prover & verifier params - let (decider_pp, decider_vp) = D::preprocess(&mut rng, &nova_params, nova.clone()).unwrap(); + let (decider_pp, decider_vp) = + D::preprocess(&mut rng, nova_params.clone(), nova.clone()).unwrap(); // serialize the Nova params. These params are the trusted setup of the commitment schemes used // (ie. KZG & Pedersen in this case) diff --git a/folding-schemes/src/folding/nova/zk.rs b/folding-schemes/src/folding/nova/zk.rs index 3ec198e..ca4506f 100644 --- a/folding-schemes/src/folding/nova/zk.rs +++ b/folding-schemes/src/folding/nova/zk.rs @@ -1,4 +1,35 @@ -// Implements nova's zero-knowledge layer, as described in https://eprint.iacr.org/2023/573.pdf +/// Implements Nova's zero-knowledge layer, as described in https://eprint.iacr.org/2023/573.pdf. +/// +/// Remark: this zk layer implementation only covers a subset of the use cases: +/// +/// We identify 3 interesting places to use the nova zk-layer: one before all the folding pipeline +/// (Use-case-1), one at the end of the folding pipeline right before the final Decider SNARK +/// proof (Use-case-2), and a third one for cases where compressed SNARK proofs are not needed, and +/// just IVC proofs (bigger than SNARK proofs) suffice (Use-case-3): +/// +/// * Use-case-1: at the beginning of the folding pipeline, right when the user has their original +/// instance prior to be folded into the running instance, the user can fold it with the +/// random-satisfying-instance to then have a blinded instance that can be sent to a server that +/// will fold it with the running instance. +/// --> In this one, the user could externalize all the IVC folding and also the Decider +/// final proof generation to a server. +/// * Use-case-2: at the end of all the IVC folding steps (after n iterations of nova.prove_step), +/// to 'blind' the IVC proof so then it can be sent to a server that will generate the final +/// decider snark proof. +/// --> In this one, the user could externalize the Decider final proof generation to a +/// server. +/// * Use-case-3: the user does not care about the Decider (final compressed SNARK proof), and +/// wants to generate a zk-proof of the IVC state to an IVC verifier (without any SNARK proof +/// involved). Note that this proof will be much bigger and expensive to verify than a Decider +/// SNARK proof. +/// +/// The current implementation covers the Use-case-3. +/// Use-case-1 can be achieved directly by a simpler version of the zk IVC scheme skipping steps +/// and implemented directly at the app level by folding the original instance with a randomized +/// instance (steps 2,3,4 from section D.4 of the [HyperNova](https://eprint.iacr.org/2023/573.pdf) +/// paper). +/// And the Use-case-2 would require a modified version of the Decider circuits. +/// use crate::folding::nova::traits::NovaR1CS; use ark_crypto_primitives::sponge::CryptographicSponge; use ark_ff::{BigInteger, PrimeField}; diff --git a/folding-schemes/src/lib.rs b/folding-schemes/src/lib.rs index 9082169..e4b22ef 100644 --- a/folding-schemes/src/lib.rs +++ b/folding-schemes/src/lib.rs @@ -219,7 +219,7 @@ pub trait Decider< fn preprocess( rng: impl RngCore + CryptoRng, - prep_param: &Self::PreprocessorParam, + prep_param: Self::PreprocessorParam, fs: FS, ) -> Result<(Self::ProverParam, Self::VerifierParam), Error>; diff --git a/solidity-verifiers/src/verifiers/nova_cyclefold.rs b/solidity-verifiers/src/verifiers/nova_cyclefold.rs index 9e896bd..e82bd1f 100644 --- a/solidity-verifiers/src/verifiers/nova_cyclefold.rs +++ b/solidity-verifiers/src/verifiers/nova_cyclefold.rs @@ -346,7 +346,7 @@ mod tests { ) .unwrap(); let decider_params = - DECIDER::preprocess(&mut rng, &nova_params.clone(), nova.clone()).unwrap(); + DECIDER::preprocess(&mut rng, nova_params.clone(), nova.clone()).unwrap(); (nova_params, decider_params) } From 1322767a1e95d3c626ae4a6194b23da90c2d7b7f Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 12 Sep 2024 15:08:53 +0100 Subject: [PATCH 4/5] Protogalaxy based IVC (#123) * Parallelize vector and matrix operations * Implement convenient methods for `NonNativeAffineVar` * Return `L_X_evals` and intermediate `phi_star`s from ProtoGalaxy prover. These values will be used as hints to the augmented circuit * Correctly use number of variables, number of constraints, and `t` * Fix the size of `F_coeffs` and `K_coeffs` for in-circuit consistency * Improve prover's performance * Make `prepare_inputs` generic * Remove redundant parameters in verifier * Move `eval_f` to arith * `u` is unnecessary in ProtoGalaxy * Convert `RelaxedR1CS` to a trait that can be used in both Nova and ProtoGalaxy * Implement several traits for ProtoGalaxy * Move `FCircuit` impls to `utils.rs` and add `DummyCircuit` * `AugmentedFCircuit` and ProtoGalaxy-based IVC * Add explanations about IVC prover and in-circuit operations * Avoid using unstable features * Rename `PROTOGALAXY` to `PG` to make clippy happy * Fix merge conflicts in `RelaxedR1CS::sample` * Fix merge conflicts in `CycleFoldCircuit` * Swap `m` and `n` for protogalaxy * Add `#[cfg(test)]` to test-only util circuits * Prefer unit struct over empty struct * Add documents to `AugmentedFCircuit` for ProtoGalaxy * Fix the names for CycleFold cricuits in ProtoGalaxy * Fix usize conversion when targeting wasm * Restrict the visibility of fields in `AugmentedFCircuit` to `pub(super)` * Make CycleFold circuits and configs public * Add docs for `ProverParams` and `VerifierParams` * Refactor `pow_i` * Fix imports * Remove lint reasons * Fix type inference --- folding-schemes/src/arith/ccs.rs | 32 +- folding-schemes/src/arith/mod.rs | 17 +- folding-schemes/src/arith/r1cs.rs | 190 ++-- .../src/folding/circuits/cyclefold.rs | 9 +- .../src/folding/circuits/nonnative/affine.rs | 46 +- .../src/folding/hypernova/circuits.rs | 55 +- .../src/folding/hypernova/decider_eth.rs | 2 +- .../folding/hypernova/decider_eth_circuit.rs | 2 +- folding-schemes/src/folding/hypernova/mod.rs | 33 +- folding-schemes/src/folding/nova/circuits.rs | 42 +- .../src/folding/nova/decider_eth.rs | 2 +- .../src/folding/nova/decider_eth_circuit.rs | 71 +- folding-schemes/src/folding/nova/mod.rs | 67 +- folding-schemes/src/folding/nova/nifs.rs | 29 +- folding-schemes/src/folding/nova/serialize.rs | 2 +- folding-schemes/src/folding/nova/traits.rs | 123 +- folding-schemes/src/folding/nova/zk.rs | 35 +- .../src/folding/protogalaxy/circuits.rs | 421 ++++++- .../src/folding/protogalaxy/folding.rs | 257 ++--- .../src/folding/protogalaxy/mod.rs | 1001 ++++++++++++++++- .../src/folding/protogalaxy/traits.rs | 78 +- .../src/folding/protogalaxy/utils.rs | 57 + folding-schemes/src/frontend/circom/mod.rs | 4 +- folding-schemes/src/frontend/mod.rs | 125 +- folding-schemes/src/frontend/utils.rs | 181 +++ folding-schemes/src/utils/vec.rs | 28 +- 26 files changed, 2218 insertions(+), 691 deletions(-) create mode 100644 folding-schemes/src/frontend/utils.rs diff --git a/folding-schemes/src/arith/ccs.rs b/folding-schemes/src/arith/ccs.rs index 3dd6b87..10425f8 100644 --- a/folding-schemes/src/arith/ccs.rs +++ b/folding-schemes/src/arith/ccs.rs @@ -36,8 +36,7 @@ pub struct CCS { } impl Arith for CCS { - /// check that a CCS structure is satisfied by a z vector. Only for testing. - fn check_relation(&self, z: &[F]) -> Result<(), Error> { + fn eval_relation(&self, z: &[F]) -> Result, Error> { let mut result = vec![F::zero(); self.m]; for i in 0..self.q { @@ -57,14 +56,7 @@ impl Arith for CCS { result = vec_add(&result, &c_M_j_z)?; } - // make sure the final vector is all zeroes - for e in result { - if !e.is_zero() { - return Err(Error::NotSatisfied); - } - } - - Ok(()) + Ok(result) } fn params_to_le_bytes(&self) -> Vec { @@ -113,7 +105,10 @@ impl CCS { #[cfg(test)] pub mod tests { use super::*; - use crate::arith::r1cs::tests::{get_test_r1cs, get_test_z as r1cs_get_test_z}; + use crate::{ + arith::r1cs::tests::{get_test_r1cs, get_test_z as r1cs_get_test_z}, + utils::vec::is_zero_vec, + }; use ark_pallas::Fr; pub fn get_test_ccs() -> CCS { @@ -124,9 +119,22 @@ pub mod tests { r1cs_get_test_z(input) } + #[test] + fn test_eval_ccs_relation() { + let ccs = get_test_ccs::(); + let mut z = get_test_z(3); + + let f_w = ccs.eval_relation(&z).unwrap(); + assert!(is_zero_vec(&f_w)); + + z[1] = Fr::from(111); + let f_w = ccs.eval_relation(&z).unwrap(); + assert!(!is_zero_vec(&f_w)); + } + /// Test that a basic CCS relation can be satisfied #[test] - fn test_ccs_relation() { + fn test_check_ccs_relation() { let ccs = get_test_ccs::(); let z = get_test_z(3); diff --git a/folding-schemes/src/arith/mod.rs b/folding-schemes/src/arith/mod.rs index e09746d..d1fa8a3 100644 --- a/folding-schemes/src/arith/mod.rs +++ b/folding-schemes/src/arith/mod.rs @@ -6,8 +6,21 @@ pub mod ccs; pub mod r1cs; pub trait Arith { - /// Checks that the given Arith structure is satisfied by a z vector. Used only for testing. - fn check_relation(&self, z: &[F]) -> Result<(), Error>; + /// Evaluate the given Arith structure at `z`, a vector of assignments, and + /// return the evaluation. + fn eval_relation(&self, z: &[F]) -> Result, Error>; + + /// Checks that the given Arith structure is satisfied by a z vector, i.e., + /// if the evaluation is a zero vector + /// + /// Used only for testing. + fn check_relation(&self, z: &[F]) -> Result<(), Error> { + if self.eval_relation(z)?.iter().all(|f| f.is_zero()) { + Ok(()) + } else { + Err(Error::NotSatisfied) + } + } /// Returns the bytes that represent the parameters, that is, the matrices sizes, the amount of /// public inputs, etc, without the matrices/polynomials values. diff --git a/folding-schemes/src/arith/r1cs.rs b/folding-schemes/src/arith/r1cs.rs index 769daca..4643cd9 100644 --- a/folding-schemes/src/arith/r1cs.rs +++ b/folding-schemes/src/arith/r1cs.rs @@ -1,15 +1,13 @@ use crate::commitment::CommitmentScheme; -use crate::folding::nova::{CommittedInstance, Witness}; use crate::RngCore; -use ark_crypto_primitives::sponge::Absorb; -use ark_ec::{CurveGroup, Group}; +use ark_ec::CurveGroup; use ark_ff::PrimeField; use ark_relations::r1cs::ConstraintSystem; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::rand::Rng; use super::Arith; -use crate::utils::vec::{hadamard, mat_vec_mul, vec_add, vec_scalar_mul, vec_sub, SparseMatrix}; +use crate::utils::vec::{hadamard, mat_vec_mul, vec_scalar_mul, vec_sub, SparseMatrix}; use crate::Error; #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] @@ -21,16 +19,24 @@ pub struct R1CS { } impl Arith for R1CS { - /// check that a R1CS structure is satisfied by a z vector. Only for testing. - fn check_relation(&self, z: &[F]) -> Result<(), Error> { + fn eval_relation(&self, z: &[F]) -> Result, Error> { + if z.len() != self.A.n_cols { + return Err(Error::NotSameLength( + "z.len()".to_string(), + z.len(), + "number of variables in R1CS".to_string(), + self.A.n_cols, + )); + } + let Az = mat_vec_mul(&self.A, z)?; let Bz = mat_vec_mul(&self.B, z)?; let Cz = mat_vec_mul(&self.C, z)?; + // Multiply Cz by z[0] (u) here, allowing this method to be reused for + // both relaxed and unrelaxed R1CS. + let uCz = vec_scalar_mul(&Cz, &z[0]); let AzBz = hadamard(&Az, &Bz)?; - if AzBz != Cz { - return Err(Error::NotSatisfied); - } - Ok(()) + vec_sub(&AzBz, &uCz) } fn params_to_le_bytes(&self) -> Vec { @@ -65,55 +71,50 @@ impl R1CS { pub fn split_z(&self, z: &[F]) -> (Vec, Vec) { (z[self.l + 1..].to_vec(), z[1..self.l + 1].to_vec()) } - - /// converts the R1CS instance into a RelaxedR1CS as described in - /// [Nova](https://eprint.iacr.org/2021/370.pdf) section 4.1. - pub fn relax(self) -> RelaxedR1CS { - RelaxedR1CS:: { - l: self.l, - E: vec![F::zero(); self.A.n_rows], - A: self.A, - B: self.B, - C: self.C, - u: F::one(), - } - } } -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct RelaxedR1CS { - pub l: usize, // io len - pub A: SparseMatrix, - pub B: SparseMatrix, - pub C: SparseMatrix, - pub u: F, - pub E: Vec, -} +pub trait RelaxedR1CS: Arith { + /// returns a dummy running instance (Witness and CommittedInstance) for the current R1CS structure + fn dummy_running_instance(&self) -> (W, U); -impl RelaxedR1CS { - /// check that a RelaxedR1CS structure is satisfied by a z vector. Only for testing. - pub fn check_relation(&self, z: &[F]) -> Result<(), Error> { - let Az = mat_vec_mul(&self.A, z)?; - let Bz = mat_vec_mul(&self.B, z)?; - let Cz = mat_vec_mul(&self.C, z)?; - let uCz = vec_scalar_mul(&Cz, &self.u); - let uCzE = vec_add(&uCz, &self.E)?; - let AzBz = hadamard(&Az, &Bz)?; - if AzBz != uCzE { - return Err(Error::NotSatisfied); + /// returns a dummy incoming instance (Witness and CommittedInstance) for the current R1CS structure + fn dummy_incoming_instance(&self) -> (W, U); + + /// checks if the given instance is relaxed + fn is_relaxed(w: &W, u: &U) -> bool; + + /// extracts `z`, the vector of variables, from the given Witness and CommittedInstance + fn extract_z(w: &W, u: &U) -> Vec; + + /// checks if the computed error terms correspond to the actual one in `w` + /// or `u` + fn check_error_terms(w: &W, u: &U, e: Vec) -> Result<(), Error>; + + /// checks the tight (unrelaxed) R1CS relation + fn check_tight_relation(&self, w: &W, u: &U) -> Result<(), Error> { + if Self::is_relaxed(w, u) { + return Err(Error::R1CSUnrelaxedFail); } - Ok(()) + let z = Self::extract_z(w, u); + self.check_relation(&z) + } + + /// checks the relaxed R1CS relation + fn check_relaxed_relation(&self, w: &W, u: &U) -> Result<(), Error> { + let z = Self::extract_z(w, u); + let e = self.eval_relation(&z)?; + Self::check_error_terms(w, u, e) } // Computes the E term, given A, B, C, z, u fn compute_E( - A: &SparseMatrix, - B: &SparseMatrix, - C: &SparseMatrix, - z: &[F], - u: &F, - ) -> Result, Error> { + A: &SparseMatrix, + B: &SparseMatrix, + C: &SparseMatrix, + z: &[C::ScalarField], + u: &C::ScalarField, + ) -> Result, Error> { let Az = mat_vec_mul(A, z)?; let Bz = mat_vec_mul(B, z)?; let AzBz = hadamard(&Az, &Bz)?; @@ -123,66 +124,9 @@ impl RelaxedR1CS { vec_sub(&AzBz, &uCz) } - pub fn check_sampled_relaxed_r1cs(&self, u: F, E: &[F], z: &[F]) -> bool { - let sampled = RelaxedR1CS { - l: self.l, - A: self.A.clone(), - B: self.B.clone(), - C: self.C.clone(), - u, - E: E.to_vec(), - }; - sampled.check_relation(z).is_ok() - } - - // Implements sampling a (committed) RelaxedR1CS - // See construction 5 in https://eprint.iacr.org/2023/573.pdf - pub fn sample( - &self, - params: &CS::ProverParams, - mut rng: impl RngCore, - ) -> Result<(CommittedInstance, Witness), Error> + fn sample(&self, params: &CS::ProverParams, rng: impl RngCore) -> Result<(W, U), Error> where - C: CurveGroup, - C: CurveGroup, - ::ScalarField: Absorb, - CS: CommitmentScheme, - { - let u = C::ScalarField::rand(&mut rng); - let rE = C::ScalarField::rand(&mut rng); - let rW = C::ScalarField::rand(&mut rng); - - let W = (0..self.A.n_cols - self.l - 1) - .map(|_| F::rand(&mut rng)) - .collect(); - let x = (0..self.l).map(|_| F::rand(&mut rng)).collect::>(); - let mut z = vec![u]; - z.extend(&x); - z.extend(&W); - - let E = RelaxedR1CS::compute_E(&self.A, &self.B, &self.C, &z, &u)?; - - debug_assert!( - z.len() == self.A.n_cols, - "Length of z is {}, while A has {} columns.", - z.len(), - self.A.n_cols - ); - - debug_assert!( - self.check_sampled_relaxed_r1cs(u, &E, &z), - "Sampled a non satisfiable relaxed R1CS, sampled u: {}, computed E: {:?}", - u, - E - ); - - let witness = Witness { E, rE, W, rW }; - let mut cm_witness = witness.commit::(params, x)?; - - // witness.commit() sets u to 1, we set it to the sampled u value - cm_witness.u = u; - Ok((cm_witness, witness)) - } + CS: CommitmentScheme; } /// extracts arkworks ConstraintSystem matrices into crate::utils::vec::SparseMatrix format as R1CS @@ -229,9 +173,13 @@ pub fn extract_w_x(cs: &ConstraintSystem) -> (Vec, Vec) #[cfg(test)] pub mod tests { use super::*; + use crate::folding::nova::{CommittedInstance, Witness}; use crate::{ commitment::pedersen::Pedersen, - utils::vec::tests::{to_F_matrix, to_F_vec}, + utils::vec::{ + is_zero_vec, + tests::{to_F_matrix, to_F_vec}, + }, }; use ark_pallas::{Fr, Projective}; @@ -242,9 +190,8 @@ pub mod tests { let r1cs = get_test_r1cs::(); let (prover_params, _) = Pedersen::::setup(rng, r1cs.A.n_rows).unwrap(); - let relaxed_r1cs = r1cs.relax(); - let sampled = - relaxed_r1cs.sample::>(&prover_params, rng); + let sampled: Result<(Witness, CommittedInstance), _> = + r1cs.sample::>(&prover_params, rng); assert!(sampled.is_ok()); } @@ -302,10 +249,23 @@ pub mod tests { } #[test] - fn test_check_relation() { + fn test_eval_r1cs_relation() { + let mut rng = ark_std::test_rng(); + let r1cs = get_test_r1cs::(); + let mut z = get_test_z::(rng.gen::() as usize); + + let f_w = r1cs.eval_relation(&z).unwrap(); + assert!(is_zero_vec(&f_w)); + + z[1] = Fr::from(111); + let f_w = r1cs.eval_relation(&z).unwrap(); + assert!(!is_zero_vec(&f_w)); + } + + #[test] + fn test_check_r1cs_relation() { let r1cs = get_test_r1cs::(); let z = get_test_z(5); r1cs.check_relation(&z).unwrap(); - r1cs.relax().check_relation(&z).unwrap(); } } diff --git a/folding-schemes/src/folding/circuits/cyclefold.rs b/folding-schemes/src/folding/circuits/cyclefold.rs index 36ba7d0..2e293e8 100644 --- a/folding-schemes/src/folding/circuits/cyclefold.rs +++ b/folding-schemes/src/folding/circuits/cyclefold.rs @@ -360,6 +360,8 @@ where } } +/// `CycleFoldConfig` allows us to customize the behavior of CycleFold circuit +/// according to the folding scheme we are working with. pub trait CycleFoldConfig { /// `N_INPUT_POINTS` specifies the number of input points that are folded in /// [`CycleFoldCircuit`] via random linear combinations. @@ -465,7 +467,10 @@ where // In multifolding schemes such as HyperNova, this is: // computed_x = [r, p_0, p_1, p_2, ..., p_n, p_folded], // where each p_i is in fact p_i.to_constraint_field() - let r_fp = Boolean::le_bits_to_fp_var(&r_bits)?; + let r_fp = r_bits + .chunks(CFG::F::MODULUS_BIT_SIZE as usize - 1) + .map(Boolean::le_bits_to_fp_var) + .collect::, _>>()?; let points_aux: Vec> = points .iter() .map(|p_i| Ok(p_i.to_constraint_field()?[..2].to_vec())) @@ -475,7 +480,7 @@ where .collect(); let computed_x: Vec> = [ - vec![r_fp], + r_fp, points_aux, p_folded.to_constraint_field()?[..2].to_vec(), ] diff --git a/folding-schemes/src/folding/circuits/nonnative/affine.rs b/folding-schemes/src/folding/circuits/nonnative/affine.rs index d172ac3..da4d9e4 100644 --- a/folding-schemes/src/folding/circuits/nonnative/affine.rs +++ b/folding-schemes/src/folding/circuits/nonnative/affine.rs @@ -1,10 +1,12 @@ -use ark_ec::{AffineRepr, CurveGroup}; +use ark_ec::{short_weierstrass::SWFlags, AffineRepr, CurveGroup}; +use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, fields::fp::FpVar, - ToConstraintFieldGadget, + R1CSVar, ToConstraintFieldGadget, }; -use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_relations::r1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; use ark_std::Zero; use core::borrow::Borrow; @@ -45,6 +47,39 @@ where } } +impl R1CSVar for NonNativeAffineVar { + type Value = C; + + fn cs(&self) -> ConstraintSystemRef { + self.x.cs().or(self.y.cs()) + } + + fn value(&self) -> Result { + debug_assert_eq!(C::BaseField::extension_degree(), 1); + + let x = ::BasePrimeField::from_le_bytes_mod_order( + &self.x.value()?.to_bytes_le(), + ); + let y = ::BasePrimeField::from_le_bytes_mod_order( + &self.y.value()?.to_bytes_le(), + ); + let mut bytes = vec![]; + x.serialize_uncompressed(&mut bytes).unwrap(); + y.serialize_with_flags( + &mut bytes, + if x.is_zero() && y.is_zero() { + SWFlags::PointAtInfinity + } else if y <= -y { + SWFlags::YIsPositive + } else { + SWFlags::YIsNegative + }, + ) + .unwrap(); + Ok(C::deserialize_uncompressed_unchecked(&bytes[..]).unwrap()) + } +} + impl ToConstraintFieldGadget for NonNativeAffineVar { // Used for converting `NonNativeAffineVar` to a vector of `FpVar` with minimum length in // the circuit. @@ -83,6 +118,10 @@ impl NonNativeAffineVar { let y = NonNativeUintVar::inputize(*y); Ok((x, y)) } + + pub fn zero() -> Self { + Self::new_constant(ConstraintSystemRef::None, C::zero()).unwrap() + } } impl AbsorbNonNative for C { @@ -105,7 +144,6 @@ impl AbsorbNonNativeGadget for NonNativeAffineVar mod tests { use super::*; use ark_pallas::{Fr, Projective}; - use ark_r1cs_std::R1CSVar; use ark_relations::r1cs::ConstraintSystem; use ark_std::UniformRand; diff --git a/folding-schemes/src/folding/hypernova/circuits.rs b/folding-schemes/src/folding/hypernova/circuits.rs index f4bb388..1c2d786 100644 --- a/folding-schemes/src/folding/hypernova/circuits.rs +++ b/folding-schemes/src/folding/hypernova/circuits.rs @@ -475,30 +475,30 @@ pub struct AugmentedFCircuit< > where for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { - pub _c2: PhantomData, - pub _gc2: PhantomData, - pub poseidon_config: PoseidonConfig>, - pub ccs: CCS, // CCS of the AugmentedFCircuit - pub pp_hash: Option>, - pub i: Option>, - pub i_usize: Option, - pub z_0: Option>, - pub z_i: Option>, - pub external_inputs: Option>, - pub U_i: Option>, - pub Us: Option>>, // other U_i's to be folded that are not the main running instance - pub u_i_C: Option, // u_i.C - pub us: Option>>, // other u_i's to be folded that are not the main incoming instance - pub U_i1_C: Option, // U_{i+1}.C - pub F: FC, // F circuit - pub x: Option>, // public input (u_{i+1}.x[0]) - pub nimfs_proof: Option>, + pub(super) _c2: PhantomData, + pub(super) _gc2: PhantomData, + pub(super) poseidon_config: PoseidonConfig>, + pub(super) ccs: CCS, // CCS of the AugmentedFCircuit + pub(super) pp_hash: Option>, + pub(super) i: Option>, + pub(super) i_usize: Option, + pub(super) z_0: Option>, + pub(super) z_i: Option>, + pub(super) external_inputs: Option>, + pub(super) U_i: Option>, + pub(super) Us: Option>>, // other U_i's to be folded that are not the main running instance + pub(super) u_i_C: Option, // u_i.C + pub(super) us: Option>>, // other u_i's to be folded that are not the main incoming instance + pub(super) U_i1_C: Option, // U_{i+1}.C + pub(super) F: FC, // F circuit + pub(super) x: Option>, // public input (u_{i+1}.x[0]) + pub(super) nimfs_proof: Option>, // cyclefold verifier on C1 - pub cf_u_i_cmW: Option, // input, cf_u_i.cmW - pub cf_U_i: Option>, // input, RelaxedR1CS CycleFold instance - pub cf_x: Option>, // public input (cf_u_{i+1}.x[1]) - pub cf_cmT: Option, + pub(super) cf_u_i_cmW: Option, // input, cf_u_i.cmW + pub(super) cf_U_i: Option>, // input, RelaxedR1CS CycleFold instance + pub(super) cf_x: Option>, // public input (cf_u_{i+1}.x[1]) + pub(super) cf_cmT: Option, } impl AugmentedFCircuit @@ -891,7 +891,7 @@ mod tests { use crate::{ arith::{ ccs::tests::{get_test_ccs, get_test_z}, - r1cs::extract_w_x, + r1cs::{extract_w_x, RelaxedR1CS}, }, commitment::{pedersen::Pedersen, CommitmentScheme}, folding::{ @@ -900,9 +900,8 @@ mod tests { utils::{compute_c, compute_sigmas_thetas}, HyperNovaCycleFoldCircuit, }, - nova::traits::NovaR1CS, }, - frontend::tests::CubicFCircuit, + frontend::utils::CubicFCircuit, transcript::poseidon::poseidon_canonical_config, utils::get_cm_coordinates, }; @@ -1216,7 +1215,7 @@ mod tests { let (cf_W_dummy, cf_U_dummy): ( CycleFoldWitness, CycleFoldCommittedInstance, - ) = cf_r1cs.dummy_instance(); + ) = cf_r1cs.dummy_running_instance(); // set the initial dummy instances let mut W_i = W_dummy.clone(); @@ -1455,9 +1454,7 @@ mod tests { u_i.check_relation(&ccs, &w_i).unwrap(); // check the CycleFold instance relation - cf_r1cs - .check_relaxed_instance_relation(&cf_W_i, &cf_U_i) - .unwrap(); + cf_r1cs.check_relaxed_relation(&cf_W_i, &cf_U_i).unwrap(); println!("augmented_f_circuit step {}: {:?}", i, start.elapsed()); } diff --git a/folding-schemes/src/folding/hypernova/decider_eth.rs b/folding-schemes/src/folding/hypernova/decider_eth.rs index 381a6b3..3e659fe 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth.rs @@ -228,7 +228,7 @@ pub mod tests { use super::*; use crate::commitment::{kzg::KZG, pedersen::Pedersen}; use crate::folding::hypernova::PreprocessorParam; - use crate::frontend::tests::CubicFCircuit; + use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; #[test] diff --git a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs index ec8c650..b4e2cf8 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs @@ -517,7 +517,7 @@ pub mod tests { use super::*; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::PreprocessorParam; - use crate::frontend::tests::CubicFCircuit; + use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; use crate::FoldingScheme; diff --git a/folding-schemes/src/folding/hypernova/mod.rs b/folding-schemes/src/folding/hypernova/mod.rs index fe75a9b..f3c90ea 100644 --- a/folding-schemes/src/folding/hypernova/mod.rs +++ b/folding-schemes/src/folding/hypernova/mod.rs @@ -21,7 +21,6 @@ use circuits::AugmentedFCircuit; use lcccs::LCCCS; use nimfs::NIMFS; -use crate::commitment::CommitmentScheme; use crate::constants::NOVA_N_BITS_RO; use crate::folding::circuits::{ cyclefold::{ @@ -30,10 +29,11 @@ use crate::folding::circuits::{ }, CF2, }; -use crate::folding::nova::{get_r1cs_from_cs, traits::NovaR1CS, PreprocessorParam}; +use crate::folding::nova::{get_r1cs_from_cs, PreprocessorParam}; use crate::frontend::FCircuit; use crate::utils::{get_cm_coordinates, pp_hash}; use crate::Error; +use crate::{arith::r1cs::RelaxedR1CS, commitment::CommitmentScheme}; use crate::{ arith::{ ccs::CCS, @@ -42,7 +42,8 @@ use crate::{ FoldingScheme, MultiFolding, }; -struct HyperNovaCycleFoldConfig { +/// Configuration for HyperNova's CycleFold circuit +pub struct HyperNovaCycleFoldConfig { _c: PhantomData, } @@ -55,7 +56,9 @@ impl CycleFoldConfig type F = C::BaseField; } -type HyperNovaCycleFoldCircuit = +/// CycleFold circuit for computing random linear combinations of group elements +/// in HyperNova instances. +pub type HyperNovaCycleFoldCircuit = CycleFoldCircuit, GC>; /// Witness for the LCCCS & CCCS, containing the w vector, and the r_w used as randomness in the Pedersen commitment. @@ -76,6 +79,7 @@ impl Witness { } } +/// Proving parameters for HyperNova-based IVC #[derive(Debug, Clone)] pub struct ProverParams where @@ -84,13 +88,18 @@ where CS1: CommitmentScheme, CS2: CommitmentScheme, { + /// Poseidon sponge configuration pub poseidon_config: PoseidonConfig, + /// Proving parameters of the underlying commitment scheme over C1 pub cs_pp: CS1::ProverParams, + /// Proving parameters of the underlying commitment scheme over C2 pub cf_cs_pp: CS2::ProverParams, - // if ccs is set, it will be used, if not, it will be computed at runtime + /// CCS of the Augmented Function circuit + /// If ccs is set, it will be used, if not, it will be computed at runtime pub ccs: Option>, } +/// Verification parameters for HyperNova-based IVC #[derive(Debug, Clone)] pub struct VerifierParams< C1: CurveGroup, @@ -99,10 +108,15 @@ pub struct VerifierParams< CS2: CommitmentScheme, const H: bool, > { + /// Poseidon sponge configuration pub poseidon_config: PoseidonConfig, + /// CCS of the Augmented step circuit pub ccs: CCS, + /// R1CS of the CycleFold circuit pub cf_r1cs: R1CS, + /// Verification parameters of the underlying commitment scheme over C1 pub cs_vp: CS1::VerifierParams, + /// Verification parameters of the underlying commitment scheme over C2 pub cf_cs_vp: CS2::VerifierParams, } @@ -282,7 +296,7 @@ where let U_i = LCCCS::::dummy(self.ccs.l, self.ccs.t, self.ccs.s); let mut u_i = CCCS::::dummy(self.ccs.l); let (_, cf_U_i): (CycleFoldWitness, CycleFoldCommittedInstance) = - self.cf_r1cs.dummy_instance(); + self.cf_r1cs.dummy_running_instance(); let sponge = PoseidonSponge::::new(&self.poseidon_config); @@ -476,7 +490,7 @@ where let w_dummy = W_dummy.clone(); let mut u_dummy = CCCS::::dummy(ccs.l); let (cf_W_dummy, cf_U_dummy): (CycleFoldWitness, CycleFoldCommittedInstance) = - cf_r1cs.dummy_instance(); + cf_r1cs.dummy_running_instance(); u_dummy.x = vec![ U_dummy.hash( &sponge, @@ -884,8 +898,7 @@ where u_i.check_relation(&vp.ccs, &w_i)?; // check CycleFold's RelaxedR1CS satisfiability - vp.cf_r1cs - .check_relaxed_instance_relation(&cf_W_i, &cf_U_i)?; + vp.cf_r1cs.check_relaxed_relation(&cf_W_i, &cf_U_i)?; Ok(()) } @@ -900,7 +913,7 @@ mod tests { use super::*; use crate::commitment::pedersen::Pedersen; - use crate::frontend::tests::CubicFCircuit; + use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; #[test] diff --git a/folding-schemes/src/folding/nova/circuits.rs b/folding-schemes/src/folding/nova/circuits.rs index 23c2dff..a506c8c 100644 --- a/folding-schemes/src/folding/nova/circuits.rs +++ b/folding-schemes/src/folding/nova/circuits.rs @@ -237,31 +237,31 @@ pub struct AugmentedFCircuit< > where for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { - pub _gc2: PhantomData, - pub poseidon_config: PoseidonConfig>, - pub pp_hash: Option>, - pub i: Option>, - pub i_usize: Option, - pub z_0: Option>, - pub z_i: Option>, - pub external_inputs: Option>, - pub u_i_cmW: Option, - pub U_i: Option>, - pub U_i1_cmE: Option, - pub U_i1_cmW: Option, - pub cmT: Option, - pub F: FC, // F circuit - pub x: Option>, // public input (u_{i+1}.x[0]) + pub(super) _gc2: PhantomData, + pub(super) poseidon_config: PoseidonConfig>, + pub(super) pp_hash: Option>, + pub(super) i: Option>, + pub(super) i_usize: Option, + pub(super) z_0: Option>, + pub(super) z_i: Option>, + pub(super) external_inputs: Option>, + pub(super) u_i_cmW: Option, + pub(super) U_i: Option>, + pub(super) U_i1_cmE: Option, + pub(super) U_i1_cmW: Option, + pub(super) cmT: Option, + pub(super) F: FC, // F circuit + pub(super) x: Option>, // public input (u_{i+1}.x[0]) // cyclefold verifier on C1 // Here 'cf1, cf2' are for each of the CycleFold circuits, corresponding to the fold of cmW and // cmE respectively - pub cf1_u_i_cmW: Option, // input - pub cf2_u_i_cmW: Option, // input - pub cf_U_i: Option>, // input - pub cf1_cmT: Option, - pub cf2_cmT: Option, - pub cf_x: Option>, // public input (u_{i+1}.x[1]) + pub(super) cf1_u_i_cmW: Option, // input + pub(super) cf2_u_i_cmW: Option, // input + pub(super) cf_U_i: Option>, // input + pub(super) cf1_cmT: Option, + pub(super) cf2_cmT: Option, + pub(super) cf_x: Option>, // public input (u_{i+1}.x[1]) } impl>, FC: FCircuit>> diff --git a/folding-schemes/src/folding/nova/decider_eth.rs b/folding-schemes/src/folding/nova/decider_eth.rs index 6377b31..570c351 100644 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ b/folding-schemes/src/folding/nova/decider_eth.rs @@ -342,7 +342,7 @@ pub mod tests { use crate::folding::nova::{ PreprocessorParam, ProverParams as NovaProverParams, VerifierParams as NovaVerifierParams, }; - use crate::frontend::tests::CubicFCircuit; + use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; #[test] diff --git a/folding-schemes/src/folding/nova/decider_eth_circuit.rs b/folding-schemes/src/folding/nova/decider_eth_circuit.rs index 0a86a68..25564a1 100644 --- a/folding-schemes/src/folding/nova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/nova/decider_eth_circuit.rs @@ -577,6 +577,8 @@ where #[cfg(test)] pub mod tests { + use std::cmp::max; + use ark_crypto_primitives::crh::{ sha256::{ constraints::{Sha256Gadget, UnitVar}, @@ -587,34 +589,61 @@ pub mod tests { use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; use ark_r1cs_std::bits::uint8::UInt8; use ark_relations::r1cs::ConstraintSystem; - use ark_std::{One, UniformRand}; + use ark_std::{ + rand::{thread_rng, Rng}, + One, UniformRand, + }; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use super::*; use crate::arith::{ r1cs::{ + extract_r1cs, extract_w_x, tests::{get_test_r1cs, get_test_z}, - {extract_r1cs, extract_w_x}, + RelaxedR1CS, }, Arith, }; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::PreprocessorParam; - use crate::frontend::tests::{CubicFCircuit, CustomFCircuit, WrapperCircuit}; + use crate::frontend::utils::{CubicFCircuit, CustomFCircuit, WrapperCircuit}; use crate::transcript::poseidon::poseidon_canonical_config; use crate::FoldingScheme; + fn prepare_instances, R: Rng>( + mut rng: R, + r1cs: &R1CS, + z: &[C::ScalarField], + ) -> (Witness, CommittedInstance) + where + C::ScalarField: Absorb, + { + let (w, x) = r1cs.split_z(z); + + let (cs_pp, _) = CS::setup(&mut rng, max(w.len(), r1cs.A.n_rows)).unwrap(); + + let mut w = Witness::new::(w, r1cs.A.n_rows, &mut rng); + w.E = r1cs.eval_relation(z).unwrap(); + let mut u = w.commit::(&cs_pp, x).unwrap(); + u.u = z[0]; + + (w, u) + } + #[test] fn test_relaxed_r1cs_small_gadget_handcrafted() { + let rng = &mut thread_rng(); + let r1cs: R1CS = get_test_r1cs(); - let rel_r1cs = r1cs.clone().relax(); - let z = get_test_z(3); + let mut z = get_test_z(3); + z[0] = Fr::rand(rng); + let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z); let cs = ConstraintSystem::::new_ref(); let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); - let EVar = Vec::>::new_witness(cs.clone(), || Ok(rel_r1cs.E)).unwrap(); - let uVar = FpVar::::new_witness(cs.clone(), || Ok(rel_r1cs.u)).unwrap(); + let EVar = Vec::>::new_witness(cs.clone(), || Ok(w.E)).unwrap(); + let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::check_native(r1csVar, EVar, uVar, zVar).unwrap(); @@ -624,6 +653,8 @@ pub mod tests { // gets as input a circuit that implements the ConstraintSynthesizer trait, and that has been // initialized. fn test_relaxed_r1cs_gadget>(circuit: CS) { + let rng = &mut thread_rng(); + let cs = ConstraintSystem::::new_ref(); circuit.generate_constraints(cs.clone()).unwrap(); @@ -634,18 +665,19 @@ pub mod tests { let r1cs = extract_r1cs::(&cs); let (w, x) = extract_w_x::(&cs); - let z = [vec![Fr::one()], x, w].concat(); + let mut z = [vec![Fr::one()], x, w].concat(); r1cs.check_relation(&z).unwrap(); - let relaxed_r1cs = r1cs.clone().relax(); - relaxed_r1cs.check_relation(&z).unwrap(); + z[0] = Fr::rand(rng); + let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z); + r1cs.check_relaxed_relation(&w, &u).unwrap(); // set new CS for the circuit that checks the RelaxedR1CS of our original circuit let cs = ConstraintSystem::::new_ref(); // prepare the inputs for our circuit let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); - let EVar = Vec::>::new_witness(cs.clone(), || Ok(relaxed_r1cs.E)).unwrap(); - let uVar = FpVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); + let EVar = Vec::>::new_witness(cs.clone(), || Ok(w.E)).unwrap(); + let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::check_native(r1csVar, EVar, uVar, zVar).unwrap(); @@ -709,6 +741,8 @@ pub mod tests { #[test] fn test_relaxed_r1cs_nonnative_circuit() { + let rng = &mut thread_rng(); + let cs = ConstraintSystem::::new_ref(); // in practice we would use CycleFoldCircuit, but is a very big circuit (when computed // non-natively inside the RelaxedR1CS circuit), so in order to have a short test we use a @@ -725,16 +759,15 @@ pub mod tests { let cs = cs.into_inner().unwrap(); let r1cs = extract_r1cs::(&cs); let (w, x) = extract_w_x::(&cs); - let z = [vec![Fq::one()], x, w].concat(); + let z = [vec![Fq::rand(rng)], x, w].concat(); - let relaxed_r1cs = r1cs.clone().relax(); + let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z); // natively let cs = ConstraintSystem::::new_ref(); let zVar = Vec::>::new_witness(cs.clone(), || Ok(z.clone())).unwrap(); - let EVar = - Vec::>::new_witness(cs.clone(), || Ok(relaxed_r1cs.clone().E)).unwrap(); - let uVar = FpVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); + let EVar = Vec::>::new_witness(cs.clone(), || Ok(w.E.clone())).unwrap(); + let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs.clone())).unwrap(); RelaxedR1CSGadget::check_native(r1csVar, EVar, uVar, zVar).unwrap(); @@ -742,8 +775,8 @@ pub mod tests { // non-natively let cs = ConstraintSystem::::new_ref(); let zVar = Vec::new_witness(cs.clone(), || Ok(z)).unwrap(); - let EVar = Vec::new_witness(cs.clone(), || Ok(relaxed_r1cs.E)).unwrap(); - let uVar = NonNativeUintVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); + let EVar = Vec::new_witness(cs.clone(), || Ok(w.E)).unwrap(); + let uVar = NonNativeUintVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::check_nonnative(r1csVar, EVar, uVar, zVar).unwrap(); diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs index be57fc9..02e6d3b 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -14,7 +14,6 @@ use ark_std::rand::RngCore; use ark_std::{One, UniformRand, Zero}; use core::marker::PhantomData; -use crate::commitment::CommitmentScheme; use crate::folding::circuits::cyclefold::{ fold_cyclefold_circuit, CycleFoldCircuit, CycleFoldCommittedInstance, CycleFoldConfig, CycleFoldWitness, @@ -25,6 +24,7 @@ use crate::transcript::{poseidon::poseidon_canonical_config, AbsorbNonNative, Tr use crate::utils::vec::is_zero_vec; use crate::Error; use crate::FoldingScheme; +use crate::{arith::r1cs::RelaxedR1CS, commitment::CommitmentScheme}; use crate::{ arith::r1cs::{extract_r1cs, extract_w_x, R1CS}, constants::NOVA_N_BITS_RO, @@ -40,8 +40,8 @@ pub mod traits; pub mod zk; use circuits::{AugmentedFCircuit, ChallengeGadget}; use nifs::NIFS; -use traits::NovaR1CS; +/// Configuration for Nova's CycleFold circuit pub struct NovaCycleFoldConfig { _c: PhantomData, } @@ -56,7 +56,9 @@ impl CycleFoldConfig for NovaCycleFoldConfig { type F = C::BaseField; } -type NovaCycleFoldCircuit = CycleFoldCircuit, GC>; +/// CycleFold circuit for computing random linear combinations of group elements +/// in Nova instances. +pub type NovaCycleFoldCircuit = CycleFoldCircuit, GC>; #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct CommittedInstance { @@ -136,10 +138,7 @@ pub struct Witness { pub rW: C::ScalarField, } -impl Witness -where - ::ScalarField: Absorb, -{ +impl Witness { pub fn new(w: Vec, e_len: usize, mut rng: impl RngCore) -> Self { let (rW, rE) = if H { ( @@ -227,6 +226,7 @@ where } } +/// Proving parameters for Nova-based IVC #[derive(Debug, Clone)] pub struct ProverParams where @@ -235,8 +235,11 @@ where CS1: CommitmentScheme, CS2: CommitmentScheme, { + /// Poseidon sponge configuration pub poseidon_config: PoseidonConfig, + /// Proving parameters of the underlying commitment scheme over C1 pub cs_pp: CS1::ProverParams, + /// Proving parameters of the underlying commitment scheme over C2 pub cf_cs_pp: CS2::ProverParams, } @@ -302,6 +305,7 @@ where } } +/// Verification parameters for Nova-based IVC #[derive(Debug, Clone)] pub struct VerifierParams where @@ -310,10 +314,15 @@ where CS1: CommitmentScheme, CS2: CommitmentScheme, { + /// Poseidon sponge configuration pub poseidon_config: PoseidonConfig, + /// R1CS of the Augmented step circuit pub r1cs: R1CS, + /// R1CS of the CycleFold circuit pub cf_r1cs: R1CS, + /// Verification parameters of the underlying commitment scheme over C1 pub cs_vp: CS1::VerifierParams, + /// Verification parameters of the underlying commitment scheme over C2 pub cf_cs_vp: CS2::VerifierParams, } @@ -553,8 +562,9 @@ where let pp_hash = vp.pp_hash()?; // setup the dummy instances - let (w_dummy, u_dummy) = r1cs.dummy_instance(); - let (cf_w_dummy, cf_u_dummy) = cf_r1cs.dummy_instance(); + let (W_dummy, U_dummy) = r1cs.dummy_running_instance(); + let (w_dummy, u_dummy) = r1cs.dummy_incoming_instance(); + let (cf_W_dummy, cf_U_dummy) = cf_r1cs.dummy_running_instance(); // W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the // R1CS that we're working with. @@ -572,13 +582,13 @@ where i: C1::ScalarField::zero(), z_0: z_0.clone(), z_i: z_0, - w_i: w_dummy.clone(), - u_i: u_dummy.clone(), - W_i: w_dummy, - U_i: u_dummy, + w_i: w_dummy, + u_i: u_dummy, + W_i: W_dummy, + U_i: U_dummy, // cyclefold running instance - cf_W_i: cf_w_dummy.clone(), - cf_U_i: cf_u_dummy.clone(), + cf_W_i: cf_W_dummy, + cf_U_i: cf_U_dummy, }) } @@ -805,10 +815,10 @@ where #[cfg(test)] { - self.cf_r1cs.check_instance_relation(&_cfW_w_i, &cfW_u_i)?; - self.cf_r1cs.check_instance_relation(&_cfE_w_i, &cfE_u_i)?; + self.cf_r1cs.check_tight_relation(&_cfW_w_i, &cfW_u_i)?; + self.cf_r1cs.check_tight_relation(&_cfE_w_i, &cfE_u_i)?; self.cf_r1cs - .check_relaxed_instance_relation(&self.cf_W_i, &self.cf_U_i)?; + .check_relaxed_relation(&self.cf_W_i, &self.cf_U_i)?; } } @@ -840,9 +850,8 @@ where #[cfg(test)] { - self.r1cs.check_instance_relation(&self.w_i, &self.u_i)?; - self.r1cs - .check_relaxed_instance_relation(&self.W_i, &self.U_i)?; + self.r1cs.check_tight_relation(&self.w_i, &self.u_i)?; + self.r1cs.check_relaxed_relation(&self.W_i, &self.U_i)?; } Ok(()) @@ -908,19 +917,13 @@ where return Err(Error::IVCVerificationFail); } - // check u_i.cmE==0, u_i.u==1 (=u_i is a un-relaxed instance) - if !u_i.cmE.is_zero() || !u_i.u.is_one() { - return Err(Error::IVCVerificationFail); - } - - // check R1CS satisfiability - vp.r1cs.check_instance_relation(&w_i, &u_i)?; + // check R1CS satisfiability, which also enforces u_i.cmE==0, u_i.u==1 + vp.r1cs.check_tight_relation(&w_i, &u_i)?; // check RelaxedR1CS satisfiability - vp.r1cs.check_relaxed_instance_relation(&W_i, &U_i)?; + vp.r1cs.check_relaxed_relation(&W_i, &U_i)?; // check CycleFold RelaxedR1CS satisfiability - vp.cf_r1cs - .check_relaxed_instance_relation(&cf_W_i, &cf_U_i)?; + vp.cf_r1cs.check_relaxed_relation(&cf_W_i, &cf_U_i)?; Ok(()) } @@ -1077,7 +1080,7 @@ pub mod tests { use super::*; use crate::commitment::pedersen::Pedersen; - use crate::frontend::tests::CubicFCircuit; + use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; /// This test tests the Nova+CycleFold IVC, and by consequence it is also testing the diff --git a/folding-schemes/src/folding/nova/nifs.rs b/folding-schemes/src/folding/nova/nifs.rs index 2f4152c..03a0ef3 100644 --- a/folding-schemes/src/folding/nova/nifs.rs +++ b/folding-schemes/src/folding/nova/nifs.rs @@ -210,10 +210,12 @@ pub mod tests { use ark_pallas::{Fr, Projective}; use ark_std::{ops::Mul, test_rng, UniformRand}; - use crate::arith::r1cs::tests::{get_test_r1cs, get_test_z}; + use crate::arith::r1cs::{ + tests::{get_test_r1cs, get_test_z}, + RelaxedR1CS, + }; use crate::commitment::pedersen::{Params as PedersenParams, Pedersen}; use crate::folding::nova::circuits::ChallengeGadget; - use crate::folding::nova::traits::NovaR1CS; use crate::transcript::poseidon::poseidon_canonical_config; #[allow(clippy::type_complexity)] @@ -316,8 +318,8 @@ pub mod tests { let u_i = u_dummy.clone(); let W_i = w_dummy.clone(); let U_i = u_dummy.clone(); - r1cs.check_relaxed_instance_relation(&w_i, &u_i).unwrap(); - r1cs.check_relaxed_instance_relation(&W_i, &U_i).unwrap(); + r1cs.check_relaxed_relation(&w_i, &u_i).unwrap(); + r1cs.check_relaxed_relation(&W_i, &U_i).unwrap(); let r_Fr = Fr::from(3_u32); @@ -334,7 +336,7 @@ pub mod tests { r_Fr, &w_i, &u_i, &W_i, &U_i, &T, cmT, ) .unwrap(); - r1cs.check_relaxed_instance_relation(&W_i1, &U_i1).unwrap(); + r1cs.check_relaxed_relation(&W_i1, &U_i1).unwrap(); } // fold 2 instances into one @@ -348,9 +350,9 @@ pub mod tests { assert_eq!(ci3_v, ci3); // check that relations hold for the 2 inputted instances and the folded one - r1cs.check_relaxed_instance_relation(&w1, &ci1).unwrap(); - r1cs.check_relaxed_instance_relation(&w2, &ci2).unwrap(); - r1cs.check_relaxed_instance_relation(&w3, &ci3).unwrap(); + r1cs.check_relaxed_relation(&w1, &ci1).unwrap(); + r1cs.check_relaxed_relation(&w2, &ci2).unwrap(); + r1cs.check_relaxed_relation(&w3, &ci3).unwrap(); // check that folded commitments from folded instance (ci) are equal to folding the // use folded rE, rW to commit w3 @@ -425,7 +427,7 @@ pub mod tests { .commit::, false>(&pedersen_params, x) .unwrap(); - r1cs.check_relaxed_instance_relation(&running_instance_w, &running_committed_instance) + r1cs.check_relaxed_relation(&running_instance_w, &running_committed_instance) .unwrap(); let num_iters = 10; @@ -438,11 +440,8 @@ pub mod tests { let incoming_committed_instance = incoming_instance_w .commit::, false>(&pedersen_params, x) .unwrap(); - r1cs.check_relaxed_instance_relation( - &incoming_instance_w, - &incoming_committed_instance, - ) - .unwrap(); + r1cs.check_relaxed_relation(&incoming_instance_w, &incoming_committed_instance) + .unwrap(); let r = Fr::rand(&mut rng); // folding challenge would come from the RO @@ -475,7 +474,7 @@ pub mod tests { &cmT, ); - r1cs.check_relaxed_instance_relation(&folded_w, &folded_committed_instance) + r1cs.check_relaxed_relation(&folded_w, &folded_committed_instance) .unwrap(); // set running_instance for next loop iteration diff --git a/folding-schemes/src/folding/nova/serialize.rs b/folding-schemes/src/folding/nova/serialize.rs index fdb070d..e5e5382 100644 --- a/folding-schemes/src/folding/nova/serialize.rs +++ b/folding-schemes/src/folding/nova/serialize.rs @@ -187,7 +187,7 @@ pub mod tests { use crate::{ commitment::{kzg::KZG, pedersen::Pedersen}, folding::nova::{Nova, PreprocessorParam}, - frontend::{tests::CubicFCircuit, FCircuit}, + frontend::{utils::CubicFCircuit, FCircuit}, transcript::poseidon::poseidon_canonical_config, FoldingScheme, }; diff --git a/folding-schemes/src/folding/nova/traits.rs b/folding-schemes/src/folding/nova/traits.rs index 8bb081f..62870d9 100644 --- a/folding-schemes/src/folding/nova/traits.rs +++ b/folding-schemes/src/folding/nova/traits.rs @@ -1,69 +1,90 @@ -use ark_crypto_primitives::sponge::Absorb; -use ark_ec::{CurveGroup, Group}; -use ark_std::One; +use ark_ec::CurveGroup; +use ark_std::{rand::RngCore, One, UniformRand}; use super::{CommittedInstance, Witness}; -use crate::arith::{r1cs::R1CS, Arith}; +use crate::arith::r1cs::{RelaxedR1CS, R1CS}; use crate::Error; -/// NovaR1CS extends R1CS methods with Nova specific methods -pub trait NovaR1CS { - /// returns a dummy instance (Witness and CommittedInstance) for the current R1CS structure - fn dummy_instance(&self) -> (Witness, CommittedInstance); - - /// checks the R1CS relation (un-relaxed) for the given Witness and CommittedInstance. - fn check_instance_relation( - &self, - W: &Witness, - U: &CommittedInstance, - ) -> Result<(), Error>; - - /// checks the Relaxed R1CS relation (corresponding to the current R1CS) for the given Witness - /// and CommittedInstance. - fn check_relaxed_instance_relation( - &self, - W: &Witness, - U: &CommittedInstance, - ) -> Result<(), Error>; -} - -impl NovaR1CS for R1CS -where - ::ScalarField: Absorb, - ::BaseField: ark_ff::PrimeField, -{ - fn dummy_instance(&self) -> (Witness, CommittedInstance) { +impl RelaxedR1CS, CommittedInstance> for R1CS { + fn dummy_running_instance(&self) -> (Witness, CommittedInstance) { let w_len = self.A.n_cols - 1 - self.l; let w_dummy = Witness::::dummy(w_len, self.A.n_rows); let u_dummy = CommittedInstance::::dummy(self.l); (w_dummy, u_dummy) } - // notice that this method does not check the commitment correctness - fn check_instance_relation( - &self, - W: &Witness, - U: &CommittedInstance, + fn dummy_incoming_instance(&self) -> (Witness, CommittedInstance) { + self.dummy_running_instance() + } + + fn is_relaxed(_w: &Witness, u: &CommittedInstance) -> bool { + u.cmE != C::zero() || u.u != C::ScalarField::one() + } + + fn extract_z(w: &Witness, u: &CommittedInstance) -> Vec { + [&[u.u][..], &u.x, &w.W].concat() + } + + fn check_error_terms( + w: &Witness, + _u: &CommittedInstance, + e: Vec, ) -> Result<(), Error> { - if U.cmE != C::zero() || U.u != C::ScalarField::one() { - return Err(Error::R1CSUnrelaxedFail); + if w.E == e { + Ok(()) + } else { + Err(Error::NotSatisfied) } - - let Z: Vec = [vec![U.u], U.x.to_vec(), W.W.to_vec()].concat(); - self.check_relation(&Z) } - // notice that this method does not check the commitment correctness - fn check_relaxed_instance_relation( + fn sample( &self, - W: &Witness, - U: &CommittedInstance, - ) -> Result<(), Error> { - let mut rel_r1cs = self.clone().relax(); - rel_r1cs.u = U.u; - rel_r1cs.E = W.E.clone(); + params: &CS::ProverParams, + mut rng: impl RngCore, + ) -> Result<(Witness, CommittedInstance), Error> + where + CS: crate::commitment::CommitmentScheme, + { + // Implements sampling a (committed) RelaxedR1CS + // See construction 5 in https://eprint.iacr.org/2023/573.pdf + let u = C::ScalarField::rand(&mut rng); + let rE = C::ScalarField::rand(&mut rng); + let rW = C::ScalarField::rand(&mut rng); + + let W = (0..self.A.n_cols - self.l - 1) + .map(|_| C::ScalarField::rand(&mut rng)) + .collect(); + let x = (0..self.l) + .map(|_| C::ScalarField::rand(&mut rng)) + .collect::>(); + let mut z = vec![u]; + z.extend(&x); + z.extend(&W); + + let E = , CommittedInstance>>::compute_E( + &self.A, &self.B, &self.C, &z, &u, + )?; + + debug_assert!( + z.len() == self.A.n_cols, + "Length of z is {}, while A has {} columns.", + z.len(), + self.A.n_cols + ); + + let witness = Witness { E, rE, W, rW }; + let mut cm_witness = witness.commit::(params, x)?; + + // witness.commit() sets u to 1, we set it to the sampled u value + cm_witness.u = u; + + debug_assert!( + self.check_relaxed_relation(&witness, &cm_witness).is_ok(), + "Sampled a non satisfiable relaxed R1CS, sampled u: {}, computed E: {:?}", + u, + witness.E + ); - let Z: Vec = [vec![U.u], U.x.to_vec(), W.W.to_vec()].concat(); - rel_r1cs.check_relation(&Z) + Ok((witness, cm_witness)) } } diff --git a/folding-schemes/src/folding/nova/zk.rs b/folding-schemes/src/folding/nova/zk.rs index ca4506f..25a216a 100644 --- a/folding-schemes/src/folding/nova/zk.rs +++ b/folding-schemes/src/folding/nova/zk.rs @@ -30,7 +30,6 @@ /// paper). /// And the Use-case-2 would require a modified version of the Decider circuits. /// -use crate::folding::nova::traits::NovaR1CS; use ark_crypto_primitives::sponge::CryptographicSponge; use ark_ff::{BigInteger, PrimeField}; use ark_std::{One, Zero}; @@ -141,9 +140,8 @@ where // d. Store folding proof let pi = FoldingProof { cmT }; - // 2. Sample a satisfying relaxed R1CS instance-witness pair (U_r, W_r) - let relaxed_instance = nova.r1cs.clone().relax(); - let (U_r, W_r) = relaxed_instance.sample::(&nova.cs_pp, &mut rng)?; + // 2. Sample a satisfying relaxed R1CS instance-witness pair (W_r, U_r) + let (W_r, U_r) = nova.r1cs.sample::(&nova.cs_pp, &mut rng)?; // 3. Fold the instance-witness pair (U_f, W_f) with (U_r, W_r) // a. Compute T @@ -280,21 +278,10 @@ where ); // 5. Check that W^{\prime}_i is a satisfying witness - let mut z = vec![U_i_prime.u]; - z.extend(&U_i_prime.x); - z.extend(&proof.W_i_prime.W); - let relaxed_r1cs = RelaxedR1CS { - l: r1cs.l, - A: r1cs.A.clone(), - B: r1cs.B.clone(), - C: r1cs.C.clone(), - u: U_i_prime.u, - E: proof.W_i_prime.E.clone(), - }; - relaxed_r1cs.check_relation(&z)?; + r1cs.check_relaxed_relation(&proof.W_i_prime, &U_i_prime)?; // 6. Check that the cyclefold instance-witness pair satisfies the cyclefold relaxed r1cs - cf_r1cs.check_relaxed_instance_relation(&proof.cf_W_i, &proof.cf_U_i)?; + cf_r1cs.check_relaxed_relation(&proof.cf_W_i, &proof.cf_U_i)?; Ok(()) } @@ -305,7 +292,7 @@ pub mod tests { use super::*; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::tests::test_ivc_opt; - use crate::frontend::tests::CubicFCircuit; + use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; use ark_bn254::{Fr, G1Projective as Projective}; use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; @@ -380,11 +367,9 @@ pub mod tests { F_circuit, 3, ); - let (sampled_committed_instance, _) = nova + let (_, sampled_committed_instance) = nova .r1cs - .clone() - .relax() - .sample::>(&nova.cs_pp, rng) + .sample::>(&nova.cs_pp, rng) .unwrap(); // proof verification fails with incorrect running instance @@ -419,11 +404,9 @@ pub mod tests { F_circuit, 3, ); - let (_, sampled_committed_witness) = nova + let (sampled_committed_witness, _) = nova .r1cs - .clone() - .relax() - .sample::>(&nova.cs_pp, rng) + .sample::>(&nova.cs_pp, rng) .unwrap(); // proof generation fails with incorrect running witness diff --git a/folding-schemes/src/folding/protogalaxy/circuits.rs b/folding-schemes/src/folding/protogalaxy/circuits.rs index 5f72690..74a7332 100644 --- a/folding-schemes/src/folding/protogalaxy/circuits.rs +++ b/folding-schemes/src/folding/protogalaxy/circuits.rs @@ -1,26 +1,46 @@ -use ark_crypto_primitives::sponge::CryptographicSponge; +use ark_crypto_primitives::sponge::{ + constraints::CryptographicSpongeVar, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, +}; use ark_ec::CurveGroup; +use ark_ff::PrimeField; use ark_poly::{univariate::DensePolynomial, EvaluationDomain, GeneralEvaluationDomain}; use ark_r1cs_std::{ alloc::AllocVar, + boolean::Boolean, + eq::EqGadget, fields::{fp::FpVar, FieldVar}, + groups::{CurveVar, GroupOpsBounds}, poly::polynomial::univariate::dense::DensePolynomialVar, + R1CSVar, ToBitsGadget, ToConstraintFieldGadget, }; -use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_std::{fmt::Debug, marker::PhantomData, One, Zero}; use super::{ folding::lagrange_polys, utils::{all_powers_var, betas_star_var, exponential_powers_var}, - CommittedInstanceVar, + CommittedInstance, CommittedInstanceVar, ProtoGalaxyCycleFoldConfig, }; use crate::{ - folding::circuits::nonnative::affine::NonNativeAffineVar, transcript::TranscriptVar, + folding::circuits::{ + cyclefold::{ + CycleFoldChallengeGadget, CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, + CycleFoldConfig, NIFSFullGadget, + }, + nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, + CF1, CF2, + }, + frontend::FCircuit, + transcript::{AbsorbNonNativeGadget, TranscriptVar}, utils::gadgets::VectorGadget, }; pub struct FoldingGadget {} impl FoldingGadget { + #[allow(clippy::type_complexity)] pub fn fold_committed_instance( transcript: &mut impl TranscriptVar, // running instance @@ -30,9 +50,8 @@ impl FoldingGadget { // polys from P F_coeffs: Vec>, K_coeffs: Vec>, - ) -> Result, SynthesisError> { + ) -> Result<(CommittedInstanceVar, Vec>), SynthesisError> { let t = instance.betas.len(); - let n = F_coeffs.len(); // absorb the committed instances transcript.absorb(instance)?; @@ -44,7 +63,7 @@ impl FoldingGadget { transcript.absorb(&F_coeffs)?; let alpha = transcript.get_challenge()?; - let alphas = all_powers_var(alpha.clone(), n); + let alphas = all_powers_var(alpha.clone(), t); // F(alpha) = e + \sum_t F_i * alpha^i let mut F_alpha = instance.e.clone(); @@ -88,34 +107,377 @@ impl FoldingGadget { let e_star = F_alpha * &L_X_evals[0] + Z_X.evaluate(&gamma)? * K_X.evaluate(&gamma)?; - let mut u_star = &instance.u * &L_X_evals[0]; let mut x_star = instance.x.mul_scalar(&L_X_evals[0])?; for i in 0..k { - u_star += &vec_instances[i].u * &L_X_evals[i + 1]; x_star = x_star.add(&vec_instances[i].x.mul_scalar(&L_X_evals[i + 1])?)?; } // return the folded instance - Ok(CommittedInstanceVar { - betas: betas_star, - // phi will be computed in CycleFold - phi: NonNativeAffineVar::new_constant(ConstraintSystemRef::None, C::zero())?, - e: e_star, - u: u_star, - x: x_star, - }) + Ok(( + CommittedInstanceVar { + betas: betas_star, + // phi will be computed in CycleFold + phi: NonNativeAffineVar::new_constant(ConstraintSystemRef::None, C::zero())?, + e: e_star, + x: x_star, + }, + L_X_evals, + )) + } +} + +pub struct AugmentationGadget; + +impl AugmentationGadget { + #[allow(clippy::type_complexity)] + pub fn prepare_and_fold_primary( + transcript: &mut impl TranscriptVar, S>, + U: CommittedInstanceVar, + u_phis: Vec>, + u_xs: Vec>>>, + new_U_phi: NonNativeAffineVar, + F_coeffs: Vec>>, + K_coeffs: Vec>>, + ) -> Result<(CommittedInstanceVar, Vec>>), SynthesisError> { + assert_eq!(u_phis.len(), u_xs.len()); + + // Prepare the incoming instances. + // For each instance `u`, we have `u.betas = []`, `u.e = 0`. + let us = u_phis + .into_iter() + .zip(u_xs) + .map(|(phi, x)| CommittedInstanceVar { + phi, + betas: vec![], + e: FpVar::zero(), + x, + }) + .collect::>(); + + // Fold the incoming instances `us` into the running instance `U`. + let (mut U, L_X_evals) = + FoldingGadget::fold_committed_instance(transcript, &U, &us, F_coeffs, K_coeffs)?; + // Notice that FoldingGadget::fold_committed_instance does not fold phi. + // We set `U.phi` to unconstrained witnesses `U_phi` here, whose + // correctness will be checked on the other curve. + U.phi = new_U_phi; + + Ok((U, L_X_evals)) + } + + pub fn prepare_and_fold_cyclefold< + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + S: CryptographicSponge, + >( + transcript: &mut PoseidonSpongeVar>, + pp_hash: FpVar>, + mut cf_U: CycleFoldCommittedInstanceVar, + cf_u_cmWs: Vec, + cf_u_xs: Vec>>>, + cf_cmTs: Vec, + ) -> Result, SynthesisError> + where + C2::BaseField: PrimeField + Absorb, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, + { + assert_eq!(cf_u_cmWs.len(), cf_u_xs.len()); + assert_eq!(cf_u_xs.len(), cf_cmTs.len()); + + // Fold the incoming CycleFold instances into the running CycleFold + // instance in a iterative way, since `NIFSFullGadget` only supports + // folding one incoming instance at a time. + for ((cmW, x), cmT) in cf_u_cmWs.into_iter().zip(cf_u_xs).zip(cf_cmTs) { + // Prepare the incoming CycleFold instance `cf_u` for the current + // iteration. + // For each CycleFold instance `cf_u`, we have `cf_u.cmE = 0`, and + // `cf_u.u = 1`. + let cf_u = CycleFoldCommittedInstanceVar { + cmE: GC2::zero(), + u: NonNativeUintVar::new_constant(ConstraintSystemRef::None, C1::BaseField::one())?, + cmW, + x, + }; + + let cf_r_bits = CycleFoldChallengeGadget::get_challenge_gadget( + transcript, + pp_hash.clone(), + cf_U.to_native_sponge_field_elements()?, + cf_u.clone(), + cmT.clone(), + )?; + // Fold the current incoming CycleFold instance `cf_u` into the + // running CycleFold instance `cf_U`. + cf_U = NIFSFullGadget::fold_committed_instance(cf_r_bits, cmT, cf_U, cf_u)?; + } + + Ok(cf_U) + } +} + +/// `AugmentedFCircuit` enhances the original step function `F`, so that it can +/// be used in recursive arguments such as IVC. +/// +/// The method for converting `F` to `AugmentedFCircuit` (`F'`) is defined in +/// [Nova](https://eprint.iacr.org/2021/370.pdf), where `AugmentedFCircuit` not +/// only invokes `F`, but also adds additional constraints for verifying the +/// correct folding of primary instances (i.e., the instances over `C1`). +/// In the paper, the primary instances are Nova's `CommittedInstance`, but we +/// extend this method to support using ProtoGalaxy's `CommittedInstance` as +/// primary instances. +/// +/// Furthermore, to reduce circuit size over `C2`, we implement the constraints +/// defined in [CycleFold](https://eprint.iacr.org/2023/1192.pdf). These extra +/// constraints verify the correct folding of CycleFold instances. +#[derive(Debug, Clone)] +pub struct AugmentedFCircuit< + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar>, + FC: FCircuit>, +> { + pub(super) _gc2: PhantomData, + pub(super) poseidon_config: PoseidonConfig>, + pub(super) pp_hash: CF1, + pub(super) i: CF1, + pub(super) i_usize: usize, + pub(super) z_0: Vec>, + pub(super) z_i: Vec>, + pub(super) external_inputs: Vec>, + pub(super) F: FC, // F circuit + pub(super) u_i_phi: C1, + pub(super) U_i: CommittedInstance, + pub(super) U_i1_phi: C1, + pub(super) F_coeffs: Vec>, + pub(super) K_coeffs: Vec>, + pub(super) x: Option>, // public input (u_{i+1}.x[0]) + + pub(super) phi_stars: Vec, + + pub(super) cf1_u_i_cmW: C2, // input + pub(super) cf2_u_i_cmW: C2, // input + pub(super) cf_U_i: CycleFoldCommittedInstance, // input + pub(super) cf1_cmT: C2, + pub(super) cf2_cmT: C2, + pub(super) cf_x: Option>, // public input (u_{i+1}.x[1]) +} + +impl>, FC: FCircuit>> + AugmentedFCircuit +where + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + pub fn empty( + poseidon_config: &PoseidonConfig>, + F_circuit: FC, + t: usize, + d: usize, + k: usize, + ) -> Self { + let u_dummy = CommittedInstance::dummy_running(2, t); + let cf_u_dummy = + CycleFoldCommittedInstance::dummy(ProtoGalaxyCycleFoldConfig::::IO_LEN); + + Self { + _gc2: PhantomData, + poseidon_config: poseidon_config.clone(), + pp_hash: CF1::::zero(), + i: CF1::::zero(), + i_usize: 0, + z_0: vec![CF1::::zero(); F_circuit.state_len()], + z_i: vec![CF1::::zero(); F_circuit.state_len()], + external_inputs: vec![CF1::::zero(); F_circuit.external_inputs_len()], + u_i_phi: C1::zero(), + U_i: u_dummy, + U_i1_phi: C1::zero(), + F_coeffs: vec![CF1::::zero(); t], + K_coeffs: vec![CF1::::zero(); d * k + 1], + phi_stars: vec![C1::zero(); k], + F: F_circuit, + x: None, + // cyclefold values + cf1_u_i_cmW: C2::zero(), + cf2_u_i_cmW: C2::zero(), + cf_U_i: cf_u_dummy, + cf1_cmT: C2::zero(), + cf2_cmT: C2::zero(), + cf_x: None, + } + } +} + +impl ConstraintSynthesizer> for AugmentedFCircuit +where + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit>, + C2::BaseField: PrimeField + Absorb, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let pp_hash = FpVar::>::new_witness(cs.clone(), || Ok(self.pp_hash))?; + let i = FpVar::>::new_witness(cs.clone(), || Ok(self.i))?; + let z_0 = Vec::>>::new_witness(cs.clone(), || Ok(self.z_0))?; + let z_i = Vec::>>::new_witness(cs.clone(), || Ok(self.z_i))?; + let external_inputs = + Vec::>>::new_witness(cs.clone(), || Ok(self.external_inputs))?; + + let u_dummy = CommittedInstance::::dummy_running(2, self.U_i.betas.len()); + let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || Ok(self.U_i))?; + let u_i_phi = NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.u_i_phi))?; + let U_i1_phi = NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.U_i1_phi))?; + let phi_stars = + Vec::>::new_witness(cs.clone(), || Ok(self.phi_stars))?; + + let cf_u_dummy = + CycleFoldCommittedInstance::dummy(ProtoGalaxyCycleFoldConfig::::IO_LEN); + let cf_U_i = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || Ok(self.cf_U_i))?; + let cf1_cmT = GC2::new_witness(cs.clone(), || Ok(self.cf1_cmT))?; + let cf2_cmT = GC2::new_witness(cs.clone(), || Ok(self.cf2_cmT))?; + + let F_coeffs = Vec::new_witness(cs.clone(), || Ok(self.F_coeffs))?; + let K_coeffs = Vec::new_witness(cs.clone(), || Ok(self.K_coeffs))?; + + // `sponge` is for digest computation. + let sponge = PoseidonSpongeVar::::new(cs.clone(), &self.poseidon_config); + // `transcript` is for challenge generation. + let mut transcript = sponge.clone(); + + // get z_{i+1} from the F circuit + let i_usize = self.i_usize; + let z_i1 = + self.F + .generate_step_constraints(cs.clone(), i_usize, z_i.clone(), external_inputs)?; + + let is_basecase = i.is_zero()?; + + // Primary Part + // P.1. Compute u_i.x + // u_i.x[0] = H(i, z_0, z_i, U_i) + let (u_i_x, _) = U_i.clone().hash( + &sponge, + pp_hash.clone(), + i.clone(), + z_0.clone(), + z_i.clone(), + )?; + // u_i.x[1] = H(cf_U_i) + let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?; + + // P.2. Prepare incoming primary instances + // P.3. Fold incoming primary instances into the running instance + let (U_i1, r) = AugmentationGadget::prepare_and_fold_primary( + &mut transcript, + U_i.clone(), + vec![u_i_phi.clone()], + vec![vec![u_i_x, cf_u_i_x]], + U_i1_phi, + F_coeffs, + K_coeffs, + )?; + + // P.4.a compute and check the first output of F' + // Base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{\bot}) + // Non-base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{i+1}) + let (u_i1_x, _) = U_i1.clone().hash( + &sponge, + pp_hash.clone(), + i + FpVar::>::one(), + z_0.clone(), + z_i1.clone(), + )?; + let (u_i1_x_base, _) = CommittedInstanceVar::new_constant(cs.clone(), u_dummy)?.hash( + &sponge, + pp_hash.clone(), + FpVar::>::one(), + z_0.clone(), + z_i1.clone(), + )?; + let x = FpVar::new_input(cs.clone(), || Ok(self.x.unwrap_or(u_i1_x_base.value()?)))?; + x.enforce_equal(&is_basecase.select(&u_i1_x_base, &u_i1_x)?)?; + + // CycleFold part + // C.1. Compute cf1_u_i.x and cf2_u_i.x + let mut r0_bits = r[0].to_bits_le()?; + let mut r1_bits = r[1].to_bits_le()?; + r0_bits.resize(C1::ScalarField::MODULUS_BIT_SIZE as usize, Boolean::FALSE); + r1_bits.resize(C1::ScalarField::MODULUS_BIT_SIZE as usize, Boolean::FALSE); + let cf1_x = [ + r0_bits + .chunks(C1::BaseField::MODULUS_BIT_SIZE as usize - 1) + .map(|bits| { + let mut bits = bits.to_vec(); + bits.resize(C1::BaseField::MODULUS_BIT_SIZE as usize, Boolean::FALSE); + NonNativeUintVar::from(&bits) + }) + .collect::>(), + vec![ + NonNativeUintVar::new_constant(cs.clone(), C1::BaseField::zero())?, + NonNativeUintVar::new_constant(cs.clone(), C1::BaseField::zero())?, + U_i.phi.x.clone(), + U_i.phi.y.clone(), + phi_stars[0].x.clone(), + phi_stars[0].y.clone(), + ], + ] + .concat(); + let cf2_x = [ + r1_bits + .chunks(C1::BaseField::MODULUS_BIT_SIZE as usize - 1) + .map(|bits| { + let mut bits = bits.to_vec(); + bits.resize(C1::BaseField::MODULUS_BIT_SIZE as usize, Boolean::FALSE); + NonNativeUintVar::from(&bits) + }) + .collect::>(), + vec![ + phi_stars[0].x.clone(), + phi_stars[0].y.clone(), + u_i_phi.x.clone(), + u_i_phi.y.clone(), + U_i1.phi.x.clone(), + U_i1.phi.y.clone(), + ], + ] + .concat(); + + // C.2. Prepare incoming CycleFold instances + // C.3. Fold incoming CycleFold instances into the running instance + let cf_U_i1 = + AugmentationGadget::prepare_and_fold_cyclefold::>>( + &mut transcript, + pp_hash.clone(), + cf_U_i, + vec![ + GC2::new_witness(cs.clone(), || Ok(self.cf1_u_i_cmW))?, + GC2::new_witness(cs.clone(), || Ok(self.cf2_u_i_cmW))?, + ], + vec![cf1_x, cf2_x], + vec![cf1_cmT, cf2_cmT], + )?; + + // Back to Primary Part + // P.4.b compute and check the second output of F' + // Base case: u_{i+1}.x[1] == H(cf_U_{\bot}) + // Non-base case: u_{i+1}.x[1] == H(cf_U_{i+1}) + let (cf_u_i1_x, _) = cf_U_i1.clone().hash(&sponge, pp_hash.clone())?; + let (cf_u_i1_x_base, _) = + CycleFoldCommittedInstanceVar::::new_constant(cs.clone(), cf_u_dummy)? + .hash(&sponge, pp_hash.clone())?; + let cf_x = FpVar::new_input(cs.clone(), || { + Ok(self.cf_x.unwrap_or(cf_u_i1_x_base.value()?)) + })?; + cf_x.enforce_equal(&is_basecase.select(&cf_u_i1_x_base, &cf_u_i1_x)?)?; + + Ok(()) } } #[cfg(test)] mod tests { - use ark_crypto_primitives::sponge::{ - constraints::CryptographicSpongeVar, - poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, - }; - use ark_pallas::{Fr, Projective}; - use ark_r1cs_std::R1CSVar; - use ark_relations::r1cs::ConstraintSystem; use std::error::Error; use super::*; @@ -125,8 +487,11 @@ mod tests { transcript::poseidon::poseidon_canonical_config, }; + use ark_bn254::{Fr, G1Projective as Projective}; + use ark_relations::r1cs::ConstraintSystem; + #[test] - fn test_fold_gadget() -> Result<(), Box> { + fn test_folding_gadget() -> Result<(), Box> { let k = 7; let (witness, instance, witnesses, instances) = prepare_inputs(k); let r1cs = get_test_r1cs::(); @@ -136,7 +501,7 @@ mod tests { let mut transcript_p = PoseidonSponge::new(&poseidon_config); let mut transcript_v = PoseidonSponge::new(&poseidon_config); - let (_, _, F_coeffs, K_coeffs) = Folding::::prove( + let (_, _, F_coeffs, K_coeffs, _, _) = Folding::::prove( &mut transcript_p, &r1cs, &instance, @@ -147,7 +512,6 @@ mod tests { let folded_instance = Folding::::verify( &mut transcript_v, - &r1cs, &instance, &instances, F_coeffs.clone(), @@ -161,7 +525,7 @@ mod tests { let F_coeffs_var = Vec::new_witness(cs.clone(), || Ok(F_coeffs))?; let K_coeffs_var = Vec::new_witness(cs.clone(), || Ok(K_coeffs))?; - let folded_instance_var = FoldingGadget::fold_committed_instance( + let (folded_instance_var, _) = FoldingGadget::fold_committed_instance( &mut transcript_var, &instance_var, &instances_var, @@ -170,7 +534,6 @@ mod tests { )?; assert_eq!(folded_instance.betas, folded_instance_var.betas.value()?); assert_eq!(folded_instance.e, folded_instance_var.e.value()?); - assert_eq!(folded_instance.u, folded_instance_var.u.value()?); assert_eq!(folded_instance.x, folded_instance_var.x.value()?); assert!(cs.is_satisfied()?); diff --git a/folding-schemes/src/folding/protogalaxy/folding.rs b/folding-schemes/src/folding/protogalaxy/folding.rs index ef9b50d..71add9a 100644 --- a/folding-schemes/src/folding/protogalaxy/folding.rs +++ b/folding-schemes/src/folding/protogalaxy/folding.rs @@ -6,18 +6,19 @@ use ark_poly::{ univariate::{DensePolynomial, SparsePolynomial}, DenseUVPolynomial, EvaluationDomain, Evaluations, GeneralEvaluationDomain, Polynomial, }; -use ark_std::{cfg_into_iter, log2, Zero}; -use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use ark_std::{cfg_into_iter, log2, One, Zero}; +use rayon::prelude::*; use std::marker::PhantomData; -use super::utils::{all_powers, betas_star, exponential_powers}; +use super::utils::{all_powers, betas_star, exponential_powers, pow_i}; use super::ProtoGalaxyError; use super::{CommittedInstance, Witness}; -use crate::arith::r1cs::R1CS; +#[cfg(test)] +use crate::arith::r1cs::RelaxedR1CS; +use crate::arith::{r1cs::R1CS, Arith}; use crate::transcript::Transcript; use crate::utils::vec::*; -use crate::utils::virtual_polynomial::bit_decompose; use crate::Error; #[derive(Clone, Debug)] @@ -48,6 +49,8 @@ where Witness, Vec, // F_X coeffs Vec, // K_X coeffs + Vec, // L_X evals + Vec, // phi_stars ), Error, > { @@ -63,19 +66,25 @@ where let k = vec_instances.len(); let t = instance.betas.len(); let n = r1cs.A.n_cols; + let m = r1cs.A.n_rows; - let z = [vec![instance.u], instance.x.clone(), w.w.clone()].concat(); + let z = [vec![C::ScalarField::one()], instance.x.clone(), w.w.clone()].concat(); if z.len() != n { return Err(Error::NotSameLength( "z.len()".to_string(), z.len(), - "n".to_string(), + "number of variables in R1CS".to_string(), // hardcoded to R1CS n, )); } - if log2(n) as usize != t { - return Err(Error::NotEqual); + if log2(m) as usize != t { + return Err(Error::NotSameLength( + "log2(number of constraints in R1CS)".to_string(), + log2(m) as usize, + "instance.betas.len()".to_string(), + t, + )); } if !(k + 1).is_power_of_two() { return Err(Error::ProtoGalaxy(ProtoGalaxyError::WrongNumInstances(k))); @@ -88,13 +97,24 @@ where let delta = transcript.get_challenge(); let deltas = exponential_powers(delta, t); - let f_z = eval_f(r1cs, &z)?; + let mut f_z = r1cs.eval_relation(&z)?; + if f_z.len() != m { + return Err(Error::NotSameLength( + "number of constraints in R1CS".to_string(), + m, + "f_z.len()".to_string(), + f_z.len(), + )); + } + f_z.resize(1 << t, C::ScalarField::zero()); // F(X) let F_X: SparsePolynomial = calc_f_from_btree(&f_z, &instance.betas, &deltas).expect("Error calculating F[x]"); let F_X_dense = DensePolynomial::from(F_X.clone()); - transcript.absorb(&F_X_dense.coeffs); + let mut F_coeffs = F_X_dense.coeffs; + F_coeffs.resize(t, C::ScalarField::zero()); + transcript.absorb(&F_coeffs); let alpha = transcript.get_challenge(); @@ -107,16 +127,14 @@ where // sanity check: check that the new randomized instance (the original instance but with // 'refreshed' randomness) satisfies the relation. #[cfg(test)] - tests::check_instance( - r1cs, + r1cs.check_relaxed_relation( + w, &CommittedInstance { phi: instance.phi, betas: betas_star.clone(), e: F_alpha, - u: instance.u, x: instance.x.clone(), }, - w, )?; let zs: Vec> = std::iter::once(z.clone()) @@ -125,12 +143,12 @@ where .iter() .zip(vec_instances) .map(|(wj, uj)| { - let zj = [vec![uj.u], uj.x.clone(), wj.w.clone()].concat(); + let zj = [vec![C::ScalarField::one()], uj.x.clone(), wj.w.clone()].concat(); if zj.len() != n { return Err(Error::NotSameLength( "zj.len()".to_string(), zj.len(), - "n".to_string(), + "number of variables in R1CS".to_string(), n, )); } @@ -153,26 +171,19 @@ where // each iteration evaluates G(h) // inner = L_0(x) * z + \sum_k L_i(x) * z_j let mut inner: Vec = vec![C::ScalarField::zero(); zs[0].len()]; - for (i, z) in zs.iter().enumerate() { + for (z, L) in zs.iter().zip(&L_X) { // Li_z_h = (Li(X)*zj)(h) = Li(h) * zj - let mut Liz_h: Vec = vec![C::ScalarField::zero(); z.len()]; + let Lh = L.evaluate(&h); for (j, zj) in z.iter().enumerate() { - Liz_h[j] = (&L_X[i] * *zj).evaluate(&h); - } - - for j in 0..inner.len() { - inner[j] += Liz_h[j]; + inner[j] += Lh * zj; } } - let f_ev = eval_f(r1cs, &inner)?; + let f_ev = r1cs.eval_relation(&inner)?; - let mut Gsum = C::ScalarField::zero(); - for (i, f_ev_i) in f_ev.iter().enumerate() { - let pow_i_betas = pow_i(i, &betas_star); - let curr = pow_i_betas * f_ev_i; - Gsum += curr; - } - G_evals[hi] = Gsum; + G_evals[hi] = cfg_into_iter!(f_ev) + .enumerate() + .map(|(i, f_ev_i)| pow_i(i, &betas_star) * f_ev_i) + .sum(); } let G_X: DensePolynomial = Evaluations::::from_vec_and_domain(G_evals, G_domain).interpolate(); @@ -190,7 +201,9 @@ where return Err(Error::ProtoGalaxy(ProtoGalaxyError::RemainderNotZero)); } - transcript.absorb(&K_X.coeffs); + let mut K_coeffs = K_X.coeffs.clone(); + K_coeffs.resize(d * k + 1, C::ScalarField::zero()); + transcript.absorb(&K_coeffs); let gamma = transcript.get_challenge(); @@ -200,17 +213,18 @@ where .map(|L| L.evaluate(&gamma)) .collect::>(); + let mut phi_stars = vec![]; + let e_star = F_alpha * L_X_evals[0] + Z_X.evaluate(&gamma) * K_X.evaluate(&gamma); let mut w_star = vec_scalar_mul(&w.w, &L_X_evals[0]); let mut r_w_star = w.r_w * L_X_evals[0]; let mut phi_star = instance.phi * L_X_evals[0]; - let mut u_star = instance.u * L_X_evals[0]; let mut x_star = vec_scalar_mul(&instance.x, &L_X_evals[0]); for i in 0..k { w_star = vec_add(&w_star, &vec_scalar_mul(&vec_w[i].w, &L_X_evals[i + 1]))?; r_w_star += vec_w[i].r_w * L_X_evals[i + 1]; + phi_stars.push(phi_star); // Push before updating. We don't need the last one phi_star += vec_instances[i].phi * L_X_evals[i + 1]; - u_star += vec_instances[i].u * L_X_evals[i + 1]; x_star = vec_add( &x_star, &vec_scalar_mul(&vec_instances[i].x, &L_X_evals[i + 1]), @@ -222,22 +236,22 @@ where betas: betas_star, phi: phi_star, e: e_star, - u: u_star, x: x_star, }, Witness { w: w_star, r_w: r_w_star, }, - F_X_dense.coeffs, - K_X.coeffs, + F_coeffs, + K_coeffs, + L_X_evals, + phi_stars, )) } /// implements the non-interactive Verifier from the folding scheme described in section 4 pub fn verify( transcript: &mut impl Transcript, - r1cs: &R1CS, // running instance instance: &CommittedInstance, // incoming instances @@ -247,7 +261,6 @@ where K_coeffs: Vec, ) -> Result, Error> { let t = instance.betas.len(); - let n = r1cs.A.n_cols; // absorb the committed instances transcript.absorb(instance); @@ -259,7 +272,7 @@ where transcript.absorb(&F_coeffs); let alpha = transcript.get_challenge(); - let alphas = all_powers(alpha, n); + let alphas = all_powers(alpha, t); // F(alpha) = e + \sum_t F_i * alpha^i let mut F_alpha = instance.e; @@ -269,6 +282,8 @@ where let betas_star = betas_star(&instance.betas, &deltas, alpha); + transcript.absorb(&K_coeffs); + let k = vec_instances.len(); let H = GeneralEvaluationDomain::::new(k + 1).ok_or(Error::NewDomainFail)?; @@ -277,8 +292,6 @@ where let K_X: DensePolynomial = DensePolynomial::::from_coefficients_vec(K_coeffs); - transcript.absorb(&K_X.coeffs); - let gamma = transcript.get_challenge(); let L_X_evals = L_X @@ -290,11 +303,9 @@ where let e_star = F_alpha * L_X_evals[0] + Z_X.evaluate(&gamma) * K_X.evaluate(&gamma); let mut phi_star = instance.phi * L_X_evals[0]; - let mut u_star = instance.u * L_X_evals[0]; let mut x_star = vec_scalar_mul(&instance.x, &L_X_evals[0]); for i in 0..k { phi_star += vec_instances[i].phi * L_X_evals[i + 1]; - u_star += vec_instances[i].u * L_X_evals[i + 1]; x_star = vec_add( &x_star, &vec_scalar_mul(&vec_instances[i].x, &L_X_evals[i + 1]), @@ -306,30 +317,11 @@ where betas: betas_star, phi: phi_star, e: e_star, - u: u_star, x: x_star, }) } } -// naive impl of pow_i for betas, assuming that betas=(b, b^2, b^4, ..., b^{2^{t-1}}) -fn pow_i(i: usize, betas: &[F]) -> F { - // WIP check if makes more sense to do it with ifs instead of arithmetic - - let n = 2_u64.pow(betas.len() as u32); - let b = bit_decompose(i as u64, n as usize); - - let mut r: F = F::one(); - for (j, beta_j) in betas.iter().enumerate() { - let mut b_j = F::zero(); - if b[j] { - b_j = F::one(); - } - r *= (F::one() - b_j) + b_j * beta_j; - } - r -} - /// calculates F[x] using the optimized binary-tree technique /// described in Claim 4.4 /// of [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) @@ -394,16 +386,6 @@ pub fn lagrange_polys( lagrange_polynomials } -// f(w) in R1CS context. For the moment we use R1CS, in the future we will abstract this with a -// trait -fn eval_f(r1cs: &R1CS, z: &[F]) -> Result, Error> { - let Az = mat_vec_mul(&r1cs.A, z)?; - let Bz = mat_vec_mul(&r1cs.B, z)?; - let Cz = mat_vec_mul(&r1cs.C, z)?; - let AzBz = hadamard(&Az, &Bz)?; - vec_sub(&AzBz, &Cz) -} - #[cfg(test)] pub mod tests { use super::*; @@ -412,38 +394,10 @@ pub mod tests { use ark_pallas::{Fr, Projective}; use ark_std::{rand::Rng, UniformRand}; - use crate::arith::r1cs::tests::{get_test_r1cs, get_test_z, get_test_z_split}; + use crate::arith::r1cs::tests::{get_test_r1cs, get_test_z_split}; use crate::commitment::{pedersen::Pedersen, CommitmentScheme}; use crate::transcript::poseidon::poseidon_canonical_config; - pub(crate) fn check_instance( - r1cs: &R1CS, - instance: &CommittedInstance, - w: &Witness, - ) -> Result<(), Error> { - let z = [vec![instance.u], instance.x.clone(), w.w.clone()].concat(); - - if instance.betas.len() != log2(z.len()) as usize { - return Err(Error::NotSameLength( - "instance.betas.len()".to_string(), - instance.betas.len(), - "log2(z.len())".to_string(), - log2(z.len()) as usize, - )); - } - - let f_z = eval_f(r1cs, &z)?; // f(z) - - let mut r = C::ScalarField::zero(); - for (i, f_z_i) in f_z.iter().enumerate() { - r += pow_i(i, &instance.betas) * f_z_i; - } - if instance.e == r { - return Ok(()); - } - Err(Error::NotSatisfied) - } - #[test] fn test_pow_i() { let mut rng = ark_std::test_rng(); @@ -459,76 +413,54 @@ pub mod tests { } } - #[test] - fn test_eval_f() { - let mut rng = ark_std::test_rng(); - let r1cs = get_test_r1cs::(); - let mut z = get_test_z::(rng.gen::() as usize); - - let f_w = eval_f(&r1cs, &z).unwrap(); - assert!(is_zero_vec(&f_w)); - - z[1] = Fr::from(111); - let f_w = eval_f(&r1cs, &z).unwrap(); - assert!(!is_zero_vec(&f_w)); - } - // k represents the number of instances to be fold, apart from the running instance #[allow(clippy::type_complexity)] - pub fn prepare_inputs( + pub fn prepare_inputs( k: usize, ) -> ( - Witness, - CommittedInstance, - Vec>, - Vec>, + Witness, + CommittedInstance, + Vec>, + Vec>, ) { let mut rng = ark_std::test_rng(); - let (u, x, w) = get_test_z_split::(rng.gen::() as usize); + let (_, x, w) = get_test_z_split::(rng.gen::() as usize); - let (pedersen_params, _) = Pedersen::::setup(&mut rng, w.len()).unwrap(); + let (pedersen_params, _) = Pedersen::::setup(&mut rng, w.len()).unwrap(); - let n = 1 + x.len() + w.len(); - let t = log2(n) as usize; + let t = log2(get_test_r1cs::().A.n_rows) as usize; - let beta = Fr::rand(&mut rng); + let beta = C::ScalarField::rand(&mut rng); let betas = exponential_powers(beta, t); - let witness = Witness:: { + let witness = Witness:: { w, - r_w: Fr::rand(&mut rng), + r_w: C::ScalarField::zero(), }; - let phi = Pedersen::::commit(&pedersen_params, &witness.w, &witness.r_w) - .unwrap(); - let instance = CommittedInstance:: { + let phi = Pedersen::::commit(&pedersen_params, &witness.w, &witness.r_w).unwrap(); + let instance = CommittedInstance:: { phi, betas: betas.clone(), - e: Fr::zero(), - u, + e: C::ScalarField::zero(), x, }; // same for the other instances - let mut witnesses: Vec> = Vec::new(); - let mut instances: Vec> = Vec::new(); + let mut witnesses: Vec> = Vec::new(); + let mut instances: Vec> = Vec::new(); #[allow(clippy::needless_range_loop)] for _ in 0..k { - let (u_i, x_i, w_i) = get_test_z_split::(rng.gen::() as usize); - let witness_i = Witness:: { + let (_, x_i, w_i) = get_test_z_split::(rng.gen::() as usize); + let witness_i = Witness:: { w: w_i, - r_w: Fr::rand(&mut rng), + r_w: C::ScalarField::zero(), }; - let phi_i = Pedersen::::commit( - &pedersen_params, - &witness_i.w, - &witness_i.r_w, - ) - .unwrap(); - let instance_i = CommittedInstance:: { + let phi_i = + Pedersen::::commit(&pedersen_params, &witness_i.w, &witness_i.r_w).unwrap(); + let instance_i = CommittedInstance:: { phi: phi_i, betas: vec![], - e: Fr::zero(), - u: u_i, + e: C::ScalarField::zero(), x: x_i, }; witnesses.push(witness_i); @@ -549,20 +481,20 @@ pub mod tests { let mut transcript_p = PoseidonSponge::::new(&poseidon_config); let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - let (folded_instance, folded_witness, F_coeffs, K_coeffs) = Folding::::prove( - &mut transcript_p, - &r1cs, - &instance, - &witness, - &instances, - &witnesses, - ) - .unwrap(); + let (folded_instance, folded_witness, F_coeffs, K_coeffs, _, _) = + Folding::::prove( + &mut transcript_p, + &r1cs, + &instance, + &witness, + &instances, + &witnesses, + ) + .unwrap(); // verifier let folded_instance_v = Folding::::verify( &mut transcript_v, - &r1cs, &instance, &instances, F_coeffs, @@ -577,7 +509,8 @@ pub mod tests { assert!(!folded_instance.e.is_zero()); // check that the folded instance satisfies the relation - check_instance(&r1cs, &folded_instance, &folded_witness).unwrap(); + r1cs.check_relaxed_relation(&folded_witness, &folded_instance) + .unwrap(); } #[test] @@ -598,7 +531,7 @@ pub mod tests { // generate the instances to be fold let (_, _, witnesses, instances) = prepare_inputs(k); - let (folded_instance, folded_witness, F_coeffs, K_coeffs) = + let (folded_instance, folded_witness, F_coeffs, K_coeffs, _, _) = Folding::::prove( &mut transcript_p, &r1cs, @@ -612,7 +545,6 @@ pub mod tests { // verifier let folded_instance_v = Folding::::verify( &mut transcript_v, - &r1cs, &running_instance, &instances, F_coeffs, @@ -626,7 +558,8 @@ pub mod tests { assert!(!folded_instance.e.is_zero()); // check that the folded instance satisfies the relation - check_instance(&r1cs, &folded_instance, &folded_witness).unwrap(); + r1cs.check_relaxed_relation(&folded_witness, &folded_instance) + .unwrap(); running_witness = folded_witness; running_instance = folded_instance; diff --git a/folding-schemes/src/folding/protogalaxy/mod.rs b/folding-schemes/src/folding/protogalaxy/mod.rs index 192f227..9ee44de 100644 --- a/folding-schemes/src/folding/protogalaxy/mod.rs +++ b/folding-schemes/src/folding/protogalaxy/mod.rs @@ -1,37 +1,120 @@ -use std::borrow::Borrow; - /// Implements the scheme described in [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) -use ark_ec::CurveGroup; -use ark_ff::PrimeField; +use ark_crypto_primitives::sponge::{ + constraints::{AbsorbGadget, CryptographicSpongeVar}, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, +}; +use ark_ec::{CurveGroup, Group}; +use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, fields::fp::FpVar, + groups::{CurveVar, GroupOpsBounds}, + R1CSVar, ToConstraintFieldGadget, +}; +use ark_relations::r1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, Namespace, SynthesisError, }; -use ark_relations::r1cs::{Namespace, SynthesisError}; -use thiserror::Error; +use ark_std::{ + borrow::Borrow, cmp::max, fmt::Debug, log2, marker::PhantomData, rand::RngCore, One, Zero, +}; +use num_bigint::BigUint; -use super::circuits::nonnative::affine::NonNativeAffineVar; +use crate::{ + arith::r1cs::{extract_r1cs, extract_w_x, RelaxedR1CS, R1CS}, + commitment::CommitmentScheme, + folding::circuits::{ + cyclefold::{ + fold_cyclefold_circuit, CycleFoldCircuit, CycleFoldCommittedInstance, CycleFoldConfig, + CycleFoldWitness, + }, + nonnative::affine::NonNativeAffineVar, + CF1, CF2, + }, + frontend::{utils::DummyCircuit, FCircuit}, + utils::{get_cm_coordinates, pp_hash}, + Error, FoldingScheme, +}; pub mod circuits; pub mod folding; pub mod traits; pub(crate) mod utils; +use circuits::AugmentedFCircuit; +use folding::Folding; + +/// Configuration for ProtoGalaxy's CycleFold circuit +pub struct ProtoGalaxyCycleFoldConfig { + _c: PhantomData, +} + +impl CycleFoldConfig for ProtoGalaxyCycleFoldConfig { + const RANDOMNESS_BIT_LENGTH: usize = C::ScalarField::MODULUS_BIT_SIZE as usize; + const N_INPUT_POINTS: usize = 2; + type C = C; + type F = C::BaseField; +} + +/// CycleFold circuit for computing random linear combinations of group elements +/// in ProtoGalaxy instances. +pub type ProtoGalaxyCycleFoldCircuit = CycleFoldCircuit, GC>; + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CommittedInstance { phi: C, betas: Vec, e: C::ScalarField, - u: C::ScalarField, x: Vec, } +impl CommittedInstance { + pub fn dummy_running(io_len: usize, t: usize) -> Self { + Self { + phi: C::zero(), + betas: vec![C::ScalarField::zero(); t], + e: C::ScalarField::zero(), + x: vec![C::ScalarField::zero(); io_len], + } + } + + pub fn dummy_incoming(io_len: usize) -> Self { + Self::dummy_running(io_len, 0) + } +} + +impl CommittedInstance +where + C::ScalarField: Absorb, + C::BaseField: PrimeField, +{ + /// hash implements the committed instance hash compatible with the gadget implemented in + /// CommittedInstanceVar.hash. + /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U_i` is the + /// `CommittedInstance`. + pub fn hash( + &self, + sponge: &PoseidonSponge, + pp_hash: C::ScalarField, + i: C::ScalarField, + z_0: Vec, + z_i: Vec, + ) -> C::ScalarField { + let mut sponge = sponge.clone(); + sponge.absorb(&pp_hash); + sponge.absorb(&i); + sponge.absorb(&z_0); + sponge.absorb(&z_i); + sponge.absorb(&self); + sponge.squeeze_field_elements(1)[0] + } +} + #[derive(Clone, Debug)] pub struct CommittedInstanceVar { phi: NonNativeAffineVar, betas: Vec>, e: FpVar, - u: FpVar, x: Vec>, } @@ -50,20 +133,98 @@ impl AllocVar, C::ScalarField> for Committed phi: NonNativeAffineVar::new_variable(cs.clone(), || Ok(u.phi), mode)?, betas: Vec::new_variable(cs.clone(), || Ok(u.betas.clone()), mode)?, e: FpVar::new_variable(cs.clone(), || Ok(u.e), mode)?, - u: FpVar::new_variable(cs.clone(), || Ok(u.u), mode)?, x: Vec::new_variable(cs.clone(), || Ok(u.x.clone()), mode)?, }) }) } } +impl R1CSVar for CommittedInstanceVar { + type Value = CommittedInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.phi + .cs() + .or(self.betas.cs()) + .or(self.e.cs()) + .or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(CommittedInstance { + phi: self.phi.value()?, + betas: self + .betas + .iter() + .map(|v| v.value()) + .collect::>()?, + e: self.e.value()?, + x: self.x.iter().map(|v| v.value()).collect::>()?, + }) + } +} + +impl CommittedInstanceVar +where + C::ScalarField: Absorb, + C::BaseField: PrimeField, +{ + /// hash implements the committed instance hash compatible with the native implementation from + /// CommittedInstance.hash. + /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U` is the + /// `CommittedInstance`. + /// Additionally it returns the vector of the field elements from the self parameters, so they + /// can be reused in other gadgets avoiding recalculating (reconstraining) them. + #[allow(clippy::type_complexity)] + pub fn hash( + self, + sponge: &PoseidonSpongeVar>, + pp_hash: FpVar>, + i: FpVar>, + z_0: Vec>>, + z_i: Vec>>, + ) -> Result<(FpVar>, Vec>>), SynthesisError> { + let mut sponge = sponge.clone(); + let U_vec = self.to_sponge_field_elements()?; + sponge.absorb(&pp_hash)?; + sponge.absorb(&i)?; + sponge.absorb(&z_0)?; + sponge.absorb(&z_i)?; + sponge.absorb(&U_vec)?; + Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec)) + } +} + #[derive(Clone, Debug)] pub struct Witness { w: Vec, r_w: F, } -#[derive(Debug, Error, PartialEq)] +impl Witness { + pub fn new(w: Vec) -> Self { + // note: at the current version, we don't use the blinding factors and we set them to 0 + // always. + // Tracking issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82 + Self { w, r_w: F::zero() } + } + + pub fn commit, C: CurveGroup>( + &self, + params: &CS::ProverParams, + x: Vec, + ) -> Result, crate::Error> { + let phi = CS::commit(params, &self.w, &self.r_w)?; + Ok(CommittedInstance { + phi, + x, + e: F::zero(), + betas: vec![], + }) + } +} + +#[derive(Debug, thiserror::Error, PartialEq)] pub enum ProtoGalaxyError { #[error("The remainder from G(X)-F(α)*L_0(X)) / Z(X) should be zero")] RemainderNotZero, @@ -76,3 +237,821 @@ pub enum ProtoGalaxyError { #[error("The lengths of β and δ do not equal: |β| = {0}, |δ|={0}")] WrongLenBetas(usize, usize), } + +/// Proving parameters for ProtoGalaxy-based IVC +#[derive(Debug, Clone)] +pub struct ProverParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + /// Poseidon sponge configuration + pub poseidon_config: PoseidonConfig, + /// Proving parameters of the underlying commitment scheme over C1 + pub cs_params: CS1::ProverParams, + /// Proving parameters of the underlying commitment scheme over C2 + pub cf_cs_params: CS2::ProverParams, +} + +/// Verification parameters for ProtoGalaxy-based IVC +#[derive(Debug, Clone)] +pub struct VerifierParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + /// Poseidon sponge configuration + pub poseidon_config: PoseidonConfig, + /// R1CS of the Augmented step circuit + pub r1cs: R1CS, + /// R1CS of the CycleFold circuit + pub cf_r1cs: R1CS, + /// Verification parameters of the underlying commitment scheme over C1 + pub cs_vp: CS1::VerifierParams, + /// Verification parameters of the underlying commitment scheme over C2 + pub cf_cs_vp: CS2::VerifierParams, +} + +impl VerifierParams +where + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + /// returns the hash of the public parameters of ProtoGalaxy + pub fn pp_hash(&self) -> Result { + // TODO (@winderica): support hiding commitments in ProtoGalaxy. + // For now, `H` is set to false. + // Tracking issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82 + pp_hash::( + &self.r1cs, + &self.cf_r1cs, + &self.cs_vp, + &self.cf_cs_vp, + &self.poseidon_config, + ) + } +} + +/// Implements ProtoGalaxy+CycleFold's IVC, described in [ProtoGalaxy] and +/// [CycleFold], following the FoldingScheme trait +/// +/// [ProtoGalaxy]: https://eprint.iacr.org/2023/1106.pdf +/// [CycleFold]: https://eprint.iacr.org/2023/1192.pdf +#[derive(Clone, Debug)] +pub struct ProtoGalaxy +where + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar>, + FC: FCircuit, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + _gc1: PhantomData, + _c2: PhantomData, + _gc2: PhantomData, + /// R1CS of the Augmented Function circuit + pub r1cs: R1CS, + /// R1CS of the CycleFold circuit + pub cf_r1cs: R1CS, + pub poseidon_config: PoseidonConfig, + /// CommitmentScheme::ProverParams over C1 + pub cs_params: CS1::ProverParams, + /// CycleFold CommitmentScheme::ProverParams, over C2 + pub cf_cs_params: CS2::ProverParams, + /// F circuit, the circuit that is being folded + pub F: FC, + /// public params hash + pub pp_hash: C1::ScalarField, + pub i: C1::ScalarField, + /// initial state + pub z_0: Vec, + /// current i-th state + pub z_i: Vec, + /// ProtoGalaxy instances + pub w_i: Witness, + pub u_i: CommittedInstance, + pub W_i: Witness, + pub U_i: CommittedInstance, + + /// CycleFold running instance + pub cf_W_i: CycleFoldWitness, + pub cf_U_i: CycleFoldCommittedInstance, +} + +impl ProtoGalaxy +where + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + ::BaseField: PrimeField, + ::BaseField: PrimeField, + C1::ScalarField: Absorb, + C2::ScalarField: Absorb, + C1: CurveGroup, + for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + /// This method computes the parameter `t` in ProtoGalaxy for folding `F'`, + /// the augmented circuit of `F` + fn compute_t( + poseidon_config: &PoseidonConfig>, + F: &FC, + d: usize, + k: usize, + ) -> Result { + // In ProtoGalaxy, prover and verifier are parameterized by `t = log(n)` + // where `n` is the number of constraints in the circuit (known as the + // mapping `f` in the paper). + // For IVC, `f` is the augmented circuit `F'`, which not only includes + // the original computation `F`, but also the in-circuit verifier of + // ProtoGalaxy. + // Therefore, `t` depends on the size of `F'`, but the size of `F'` in + // turn depends on `t`. + // To address this circular dependency, we first find `t_lower_bound`, + // the lower bound of `t`. Then we incrementally increase `t` and build + // the circuit `F'` with `t` as ProtoGalaxy's parameter, until `t` is + // the smallest integer that equals the logarithm of the number of + // constraints. + + // For `t_lower_bound`, we configure `F'` with `t = 1` and compute log2 + // of the size of `F'`. + let state_len = F.state_len(); + let external_inputs_len = F.external_inputs_len(); + + // `F'` includes `F` and `ProtoGalaxy.V`, where `F` might be costly. + // Observing that the cost of `F` is constant with respect to `t`, we + // separately compute `step_constraints`, the size of `F`. + // Later, we only need to re-run the rest of `F'` with updated `t` to + // get the size of `F'`. + let cs = ConstraintSystem::::new_ref(); + F.generate_step_constraints( + cs.clone(), + 0, + Vec::new_witness(cs.clone(), || Ok(vec![Zero::zero(); state_len]))?, + Vec::new_witness(cs.clone(), || Ok(vec![Zero::zero(); external_inputs_len]))?, + )?; + let step_constraints = cs.num_constraints(); + + // Create a dummy circuit with the same state length and external inputs + // length as `F`, which replaces `F` in the augmented circuit `F'`. + let dummy_circuit: DummyCircuit = + FCircuit::::new((state_len, external_inputs_len)).unwrap(); + + // Compute `augmentation_constraints`, the size of `F'` without `F`. + let cs = ConstraintSystem::::new_ref(); + AugmentedFCircuit::::empty( + poseidon_config, + dummy_circuit.clone(), + 1, + d, + k, + ) + .generate_constraints(cs.clone())?; + let augmentation_constraints = cs.num_constraints(); + + // The sum of `step_constraints` and `augmentation_constraints` is the + // size of `F'` with `t = 1`, and hence the actual `t` should have lower + // bound `log2(step_constraints + augmentation_constraints)`. + let t_lower_bound = log2(step_constraints + augmentation_constraints) as usize; + // Optimization: we in fact only need to try two values of `t`. + // This is because increasing `t` will only slightly affect the size of + // `F'` (more specifically, the size of `F'` will never be doubled). + // Thus, `t_lower_bound` (the log2 size of `F'` with `t = 1`) is very + // close to the actual `t` (either `t` or `t - 1`). + let t_upper_bound = t_lower_bound + 1; + + for t in t_lower_bound..=t_upper_bound { + let cs = ConstraintSystem::::new_ref(); + AugmentedFCircuit::::empty( + poseidon_config, + dummy_circuit.clone(), + t, + d, + k, + ) + .generate_constraints(cs.clone())?; + let augmentation_constraints = cs.num_constraints(); + if t == log2(step_constraints + augmentation_constraints) as usize { + return Ok(t); + } + } + unreachable!() + } +} + +impl FoldingScheme + for ProtoGalaxy +where + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + ::BaseField: PrimeField, + ::BaseField: PrimeField, + C1::ScalarField: Absorb, + C2::ScalarField: Absorb, + C1: CurveGroup, + for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + type PreprocessorParam = (PoseidonConfig>, FC); + type ProverParam = ProverParams; + type VerifierParam = VerifierParams; + type RunningInstance = (CommittedInstance, Witness); + type IncomingInstance = (CommittedInstance, Witness); + type MultiCommittedInstanceWithWitness = (CommittedInstance, Witness); + type CFInstance = (CycleFoldCommittedInstance, CycleFoldWitness); + + fn preprocess( + mut rng: impl RngCore, + (poseidon_config, F): &Self::PreprocessorParam, + ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { + // We fix `k`, the number of incoming instances, to 1, because + // multi-instances folding is not supported yet. + // TODO (@winderica): Support multi-instances folding and make `k` a + // constant generic parameter (as in HyperNova) + // Tracking issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82 + let k = 1; + // `d`, the degree of the constraint system, is set to 2, as we only + // support R1CS for now, whose highest degree is 2. + let d = 2; + let t = Self::compute_t(poseidon_config, F, d, k)?; + + // prepare the circuit to obtain its R1CS + let cs = ConstraintSystem::::new_ref(); + let cs2 = ConstraintSystem::::new_ref(); + + let augmented_F_circuit = + AugmentedFCircuit::::empty(poseidon_config, F.clone(), t, d, k); + let cf_circuit = ProtoGalaxyCycleFoldCircuit::::empty(); + + augmented_F_circuit.generate_constraints(cs.clone())?; + cs.finalize(); + let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; + let r1cs = extract_r1cs::(&cs); + + cf_circuit.generate_constraints(cs2.clone())?; + cs2.finalize(); + let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; + let cf_r1cs = extract_r1cs::(&cs2); + + let (cs_pp, cs_vp) = CS1::setup(&mut rng, r1cs.A.n_rows)?; + let (cf_cs_pp, cf_cs_vp) = CS2::setup(&mut rng, max(cf_r1cs.A.n_rows, cf_r1cs.A.n_cols))?; + + Ok(( + Self::ProverParam { + poseidon_config: poseidon_config.clone(), + cs_params: cs_pp, + cf_cs_params: cf_cs_pp, + }, + Self::VerifierParam { + poseidon_config: poseidon_config.clone(), + r1cs, + cf_r1cs, + cs_vp, + cf_cs_vp, + }, + )) + } + + /// Initializes the ProtoGalaxy+CycleFold's IVC for the given parameters and + /// initial state `z_0`. + fn init( + (pp, vp): &(Self::ProverParam, Self::VerifierParam), + F: FC, + z_0: Vec, + ) -> Result { + // compute the public params hash + let pp_hash = vp.pp_hash()?; + + // setup the dummy instances + let (W_dummy, U_dummy) = vp.r1cs.dummy_running_instance(); + let (w_dummy, u_dummy) = vp.r1cs.dummy_incoming_instance(); + let (cf_W_dummy, cf_U_dummy) = vp.cf_r1cs.dummy_running_instance(); + + // W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the + // R1CS that we're working with. + Ok(Self { + _gc1: PhantomData, + _c2: PhantomData, + _gc2: PhantomData, + r1cs: vp.r1cs.clone(), + cf_r1cs: vp.cf_r1cs.clone(), + poseidon_config: pp.poseidon_config.clone(), + cs_params: pp.cs_params.clone(), + cf_cs_params: pp.cf_cs_params.clone(), + F, + pp_hash, + i: C1::ScalarField::zero(), + z_0: z_0.clone(), + z_i: z_0, + w_i: w_dummy, + u_i: u_dummy, + W_i: W_dummy, + U_i: U_dummy, + // cyclefold running instance + cf_W_i: cf_W_dummy, + cf_U_i: cf_U_dummy, + }) + } + + /// Implements IVC.P of ProtoGalaxy+CycleFold + fn prove_step( + &mut self, + mut rng: impl RngCore, + external_inputs: Vec, + _other_instances: Option, + ) -> Result<(), Error> { + // Multi-instances folding is not supported yet. + if _other_instances.is_some() { + return Err(Error::NoMultiInstances); + } + // We fix `k`, the number of incoming instances, to 1, because + // multi-instances folding is not supported yet. + // TODO (@winderica): Support multi-instances folding and make `k` a + // constant generic parameter (as in HyperNova) + // Tracking issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82 + let k = 1; + // `d`, the degree of the constraint system, is set to 2, as we only + // support R1CS for now, whose highest degree is 2. + let d = 2; + + // `sponge` is for digest computation. + let sponge = PoseidonSponge::::new(&self.poseidon_config); + // `transcript` is for challenge generation. + let mut transcript_prover = sponge.clone(); + + let mut augmented_F_circuit: AugmentedFCircuit; + + if self.z_i.len() != self.F.state_len() { + return Err(Error::NotSameLength( + "z_i.len()".to_string(), + self.z_i.len(), + "F.state_len()".to_string(), + self.F.state_len(), + )); + } + if external_inputs.len() != self.F.external_inputs_len() { + return Err(Error::NotSameLength( + "F.external_inputs_len()".to_string(), + self.F.external_inputs_len(), + "external_inputs.len()".to_string(), + external_inputs.len(), + )); + } + + let i_bn: BigUint = self.i.into(); + let i_usize: usize = i_bn.try_into().map_err(|_| Error::MaxStep)?; + + let z_i1 = self + .F + .step_native(i_usize, self.z_i.clone(), external_inputs.clone())?; + + // folded instance output (public input, x) + // u_{i+1}.x[0] = H(i+1, z_0, z_{i+1}, U_{i+1}) + let u_i1_x: C1::ScalarField; + // u_{i+1}.x[1] = H(cf_U_{i+1}) + let cf_u_i1_x: C1::ScalarField; + + if self.i.is_zero() { + // Take extra care of the base case + // `U_{i+1}` (i.e., `U_1`) is fixed to `U_dummy`, so we just use + // `self.U_i = U_0 = U_dummy`. + u_i1_x = self.U_i.hash( + &sponge, + self.pp_hash, + self.i + C1::ScalarField::one(), + self.z_0.clone(), + z_i1.clone(), + ); + // `cf_U_{i+1}` (i.e., `cf_U_1`) is fixed to `cf_U_dummy`, so we + // just use `self.cf_U_i = cf_U_0 = cf_U_dummy`. + cf_u_i1_x = self.cf_U_i.hash_cyclefold(&sponge, self.pp_hash); + + augmented_F_circuit = AugmentedFCircuit::empty( + &self.poseidon_config, + self.F.clone(), + self.U_i.betas.len(), + d, + k, + ); + augmented_F_circuit.pp_hash = self.pp_hash; + augmented_F_circuit.z_0.clone_from(&self.z_0); + augmented_F_circuit.z_i.clone_from(&self.z_i); + augmented_F_circuit + .external_inputs + .clone_from(&external_inputs); + + // There is no need to update `self.U_i` etc. as they are unchanged. + } else { + // Primary part: + // Compute `U_{i+1}` by folding `u_i` into `U_i`. + let (U_i1, W_i1, F_coeffs, K_coeffs, L_evals, phi_stars) = Folding::prove( + &mut transcript_prover, + &self.r1cs, + &self.U_i, + &self.W_i, + &[self.u_i.clone()], + &[self.w_i.clone()], + )?; + + // CycleFold part: + // get the vector used as public inputs 'x' in the CycleFold circuit + let mut r0_bits = L_evals[0].into_bigint().to_bits_le(); + let mut r1_bits = L_evals[1].into_bigint().to_bits_le(); + r0_bits.resize(C1::ScalarField::MODULUS_BIT_SIZE as usize, false); + r1_bits.resize(C1::ScalarField::MODULUS_BIT_SIZE as usize, false); + + // cyclefold circuit for enforcing: + // 0 + U_i.phi * L_evals[0] == phi_stars[0] + let cf1_u_i_x = [ + r0_bits + .chunks(C1::BaseField::MODULUS_BIT_SIZE as usize - 1) + .map(BigInteger::from_bits_le) + .map(C1::BaseField::from_bigint) + .collect::>>() + .unwrap(), + get_cm_coordinates(&C1::zero()), + get_cm_coordinates(&self.U_i.phi), + get_cm_coordinates(&phi_stars[0]), + ] + .concat(); + let cf1_circuit = ProtoGalaxyCycleFoldCircuit:: { + _gc: PhantomData, + r_bits: Some(r0_bits), + points: Some(vec![C1::zero(), self.U_i.phi]), + x: Some(cf1_u_i_x.clone()), + }; + + // cyclefold circuit for enforcing: + // phi_stars[0] + u_i.phi * L_evals[1] == U_i1.phi + // i.e., U_i.phi * L_evals[0] + u_i.phi * L_evals[1] == U_i1.phi + let cf2_u_i_x = [ + r1_bits + .chunks(C1::BaseField::MODULUS_BIT_SIZE as usize - 1) + .map(BigInteger::from_bits_le) + .map(C1::BaseField::from_bigint) + .collect::>>() + .unwrap(), + get_cm_coordinates(&phi_stars[0]), + get_cm_coordinates(&self.u_i.phi), + get_cm_coordinates(&U_i1.phi), + ] + .concat(); + let cf2_circuit = ProtoGalaxyCycleFoldCircuit:: { + _gc: PhantomData, + r_bits: Some(r1_bits), + points: Some(vec![phi_stars[0], self.u_i.phi]), + x: Some(cf2_u_i_x.clone()), + }; + + // fold self.cf_U_i + cf1_U -> folded running with cf1 + let (_cf1_w_i, cf1_u_i, cf1_W_i1, cf1_U_i1, cf1_cmT, _) = self.fold_cyclefold_circuit( + &mut transcript_prover, + self.cf_W_i.clone(), // CycleFold running instance witness + self.cf_U_i.clone(), // CycleFold running instance + cf1_u_i_x, + cf1_circuit, + &mut rng, + )?; + // fold [the output from folding self.cf_U_i + cf1_U] + cf2_U = folded_running_with_cf1 + cf2 + let (_cf2_w_i, cf2_u_i, cf_W_i1, cf_U_i1, cf2_cmT, _) = self.fold_cyclefold_circuit( + &mut transcript_prover, + cf1_W_i1, + cf1_U_i1.clone(), + cf2_u_i_x, + cf2_circuit, + &mut rng, + )?; + + // Derive `u_{i+1}.x[0], u_{i+1}.x[1]` by hashing folded instances + u_i1_x = U_i1.hash( + &sponge, + self.pp_hash, + self.i + C1::ScalarField::one(), + self.z_0.clone(), + z_i1.clone(), + ); + cf_u_i1_x = cf_U_i1.hash_cyclefold(&sponge, self.pp_hash); + + augmented_F_circuit = AugmentedFCircuit { + _gc2: PhantomData, + poseidon_config: self.poseidon_config.clone(), + pp_hash: self.pp_hash, + i: self.i, + i_usize, + z_0: self.z_0.clone(), + z_i: self.z_i.clone(), + external_inputs: external_inputs.clone(), + u_i_phi: self.u_i.phi, + U_i: self.U_i.clone(), + U_i1_phi: U_i1.phi, + F_coeffs: F_coeffs.clone(), + K_coeffs: K_coeffs.clone(), + phi_stars, + F: self.F.clone(), + x: Some(u_i1_x), + // cyclefold values + cf1_u_i_cmW: cf1_u_i.cmW, + cf2_u_i_cmW: cf2_u_i.cmW, + cf_U_i: self.cf_U_i.clone(), + cf1_cmT, + cf2_cmT, + cf_x: Some(cf_u_i1_x), + }; + + #[cfg(test)] + { + let mut transcript_verifier = sponge.clone(); + assert_eq!( + Folding::verify( + &mut transcript_verifier, + &self.U_i, + &[self.u_i.clone()], + F_coeffs, + K_coeffs + )?, + U_i1 + ); + self.cf_r1cs.check_tight_relation(&_cf1_w_i, &cf1_u_i)?; + self.cf_r1cs.check_tight_relation(&_cf2_w_i, &cf2_u_i)?; + self.cf_r1cs + .check_relaxed_relation(&self.cf_W_i, &self.cf_U_i)?; + } + + self.W_i = W_i1; + self.U_i = U_i1; + self.cf_W_i = cf_W_i1; + self.cf_U_i = cf_U_i1; + } + + let cs = ConstraintSystem::::new_ref(); + + augmented_F_circuit.generate_constraints(cs.clone())?; + + #[cfg(test)] + assert!(cs.is_satisfied().unwrap()); + + let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; + let (w_i1, x_i1) = extract_w_x::(&cs); + if x_i1[0] != u_i1_x || x_i1[1] != cf_u_i1_x { + return Err(Error::NotEqual); + } + + #[cfg(test)] + if x_i1.len() != 2 { + return Err(Error::NotExpectedLength(x_i1.len(), 2)); + } + + // set values for next iteration + self.i += C1::ScalarField::one(); + self.z_i = z_i1; + self.w_i = Witness::new(w_i1); + self.u_i = self.w_i.commit::(&self.cs_params, x_i1)?; + + #[cfg(test)] + { + self.r1cs.check_tight_relation(&self.w_i, &self.u_i)?; + self.r1cs.check_relaxed_relation(&self.W_i, &self.U_i)?; + } + + Ok(()) + } + + fn state(&self) -> Vec { + self.z_i.clone() + } + fn instances( + &self, + ) -> ( + Self::RunningInstance, + Self::IncomingInstance, + Self::CFInstance, + ) { + ( + (self.U_i.clone(), self.W_i.clone()), + (self.u_i.clone(), self.w_i.clone()), + (self.cf_U_i.clone(), self.cf_W_i.clone()), + ) + } + + /// Implements IVC.V of ProtoGalaxy+CycleFold + fn verify( + vp: Self::VerifierParam, + z_0: Vec, // initial state + z_i: Vec, // last state + num_steps: C1::ScalarField, + running_instance: Self::RunningInstance, + incoming_instance: Self::IncomingInstance, + cyclefold_instance: Self::CFInstance, + ) -> Result<(), Error> { + let sponge = PoseidonSponge::::new(&vp.poseidon_config); + + let (U_i, W_i) = running_instance; + let (u_i, w_i) = incoming_instance; + let (cf_U_i, cf_W_i) = cyclefold_instance; + + if u_i.x.len() != 2 || U_i.x.len() != 2 { + return Err(Error::IVCVerificationFail); + } + + let pp_hash = vp.pp_hash()?; + + // check that u_i's output points to the running instance + // u_i.X[0] == H(i, z_0, z_i, U_i) + let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, z_0, z_i.clone()); + if expected_u_i_x != u_i.x[0] { + return Err(Error::IVCVerificationFail); + } + // u_i.X[1] == H(cf_U_i) + let expected_cf_u_i_x = cf_U_i.hash_cyclefold(&sponge, pp_hash); + if expected_cf_u_i_x != u_i.x[1] { + return Err(Error::IVCVerificationFail); + } + + // check R1CS satisfiability + vp.r1cs.check_tight_relation(&w_i, &u_i)?; + // check RelaxedR1CS satisfiability + vp.r1cs.check_relaxed_relation(&W_i, &U_i)?; + + // check CycleFold RelaxedR1CS satisfiability + vp.cf_r1cs.check_relaxed_relation(&cf_W_i, &cf_U_i)?; + + Ok(()) + } +} + +impl ProtoGalaxy +where + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + ::BaseField: PrimeField, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + ::ScalarField: Absorb, + C1: CurveGroup, + for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + // folds the given cyclefold circuit and its instances + #[allow(clippy::type_complexity)] + fn fold_cyclefold_circuit( + &self, + transcript: &mut PoseidonSponge, + cf_W_i: CycleFoldWitness, // witness of the running instance + cf_U_i: CycleFoldCommittedInstance, // running instance + cf_u_i_x: Vec, + cf_circuit: ProtoGalaxyCycleFoldCircuit, + rng: &mut impl RngCore, + ) -> Result< + ( + CycleFoldWitness, + CycleFoldCommittedInstance, // u_i + CycleFoldWitness, // W_i1 + CycleFoldCommittedInstance, // U_i1 + C2, // cmT + C2::ScalarField, // r_Fq + ), + Error, + > { + fold_cyclefold_circuit::, C1, GC1, C2, GC2, CS2, false>( + transcript, + self.cf_r1cs.clone(), + self.cf_cs_params.clone(), + self.pp_hash, + cf_W_i, + cf_U_i, + cf_u_i_x, + cf_circuit, + rng, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective}; + use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; + use ark_std::test_rng; + use rayon::prelude::*; + + use crate::{ + commitment::{kzg::KZG, pedersen::Pedersen}, + frontend::utils::CubicFCircuit, + transcript::poseidon::poseidon_canonical_config, + }; + + /// This test tests the ProtoGalaxy+CycleFold IVC, and by consequence it is + /// also testing the AugmentedFCircuit + #[test] + fn test_ivc() { + let poseidon_config = poseidon_canonical_config::(); + + let F_circuit = CubicFCircuit::::new(()).unwrap(); + + // run the test using Pedersen commitments on both sides of the curve cycle + test_ivc_opt::, Pedersen>( + poseidon_config.clone(), + F_circuit, + ); + // run the test using KZG for the commitments on the main curve, and Pedersen for the + // commitments on the secondary curve + test_ivc_opt::, Pedersen>(poseidon_config, F_circuit); + } + + // test_ivc allowing to choose the CommitmentSchemes + fn test_ivc_opt, CS2: CommitmentScheme>( + poseidon_config: PoseidonConfig, + F_circuit: CubicFCircuit, + ) { + type PG = + ProtoGalaxy, CS1, CS2>; + + let params = + PG::::preprocess(&mut test_rng(), &(poseidon_config, F_circuit)).unwrap(); + + let z_0 = vec![Fr::from(3_u32)]; + let mut protogalaxy = PG::init(¶ms, F_circuit, z_0.clone()).unwrap(); + + let num_steps: usize = 3; + for _ in 0..num_steps { + protogalaxy + .prove_step(&mut test_rng(), vec![], None) + .unwrap(); + } + assert_eq!(Fr::from(num_steps as u32), protogalaxy.i); + + let (running_instance, incoming_instance, cyclefold_instance) = protogalaxy.instances(); + PG::::verify( + params.1, + z_0, + protogalaxy.z_i, + protogalaxy.i, + running_instance, + incoming_instance, + cyclefold_instance, + ) + .unwrap(); + } + + #[ignore] + #[test] + fn test_t_bounds() { + let d = 2; + let k = 1; + + let poseidon_config = poseidon_canonical_config::(); + for state_len in [1, 10, 100] { + for external_inputs_len in [1, 10, 100] { + let dummy_circuit: DummyCircuit = + FCircuit::::new((state_len, external_inputs_len)).unwrap(); + + let costs = (1..32) + .into_par_iter() + .map(|t| { + let cs = ConstraintSystem::::new_ref(); + AugmentedFCircuit::::empty( + &poseidon_config, + dummy_circuit.clone(), + t, + d, + k, + ) + .generate_constraints(cs.clone()) + .unwrap(); + cs.num_constraints() + }) + .collect::>(); + + for t_lower_bound in log2(costs[0]) as usize..32 { + let num_constraints = + (1 << t_lower_bound) - costs[0] + costs[t_lower_bound - 1]; + let t = log2(num_constraints) as usize; + assert!(t == t_lower_bound || t == t_lower_bound + 1); + } + } + } + } +} diff --git a/folding-schemes/src/folding/protogalaxy/traits.rs b/folding-schemes/src/folding/protogalaxy/traits.rs index ef45b31..d735f6e 100644 --- a/folding-schemes/src/folding/protogalaxy/traits.rs +++ b/folding-schemes/src/folding/protogalaxy/traits.rs @@ -3,9 +3,15 @@ use ark_ec::CurveGroup; use ark_ff::PrimeField; use ark_r1cs_std::{fields::fp::FpVar, uint8::UInt8, ToConstraintFieldGadget}; use ark_relations::r1cs::SynthesisError; +use ark_std::{cfg_iter, log2, rand::RngCore, One, Zero}; +use rayon::prelude::*; -use super::{CommittedInstance, CommittedInstanceVar}; -use crate::transcript::AbsorbNonNative; +use super::{utils::pow_i, CommittedInstance, CommittedInstanceVar, Witness}; +use crate::{ + arith::r1cs::{RelaxedR1CS, R1CS}, + transcript::AbsorbNonNative, + Error, +}; // Implements the trait for absorbing ProtoGalaxy's CommittedInstance. impl Absorb for CommittedInstance @@ -22,7 +28,6 @@ where .to_sponge_field_elements(dest); self.betas.to_sponge_field_elements(dest); self.e.to_sponge_field_elements(dest); - self.u.to_sponge_field_elements(dest); self.x.to_sponge_field_elements(dest); } } @@ -38,9 +43,74 @@ impl AbsorbGadget for CommittedInstanceVar { self.phi.to_constraint_field()?, self.betas.to_sponge_field_elements()?, self.e.to_sponge_field_elements()?, - self.u.to_sponge_field_elements()?, self.x.to_sponge_field_elements()?, ] .concat()) } } + +impl RelaxedR1CS, CommittedInstance> + for R1CS +{ + fn dummy_running_instance(&self) -> (Witness, CommittedInstance) { + let w_len = self.A.n_cols - 1 - self.l; + let w_dummy = Witness::new(vec![C::ScalarField::zero(); w_len]); + let u_dummy = CommittedInstance::::dummy_running(self.l, log2(self.A.n_rows) as usize); + (w_dummy, u_dummy) + } + + fn dummy_incoming_instance(&self) -> (Witness, CommittedInstance) { + let w_len = self.A.n_cols - 1 - self.l; + let w_dummy = Witness::new(vec![C::ScalarField::zero(); w_len]); + let u_dummy = CommittedInstance::::dummy_incoming(self.l); + (w_dummy, u_dummy) + } + + fn is_relaxed(_w: &Witness, u: &CommittedInstance) -> bool { + u.e != C::ScalarField::zero() || !u.betas.is_empty() + } + + fn extract_z(w: &Witness, u: &CommittedInstance) -> Vec { + [&[C::ScalarField::one()][..], &u.x, &w.w].concat() + } + + fn check_error_terms( + _w: &Witness, + u: &CommittedInstance, + e: Vec, + ) -> Result<(), Error> { + if u.betas.len() != log2(e.len()) as usize { + return Err(Error::NotSameLength( + "instance.betas.len()".to_string(), + u.betas.len(), + "log2(e.len())".to_string(), + log2(e.len()) as usize, + )); + } + + let r = cfg_iter!(e) + .enumerate() + .map(|(i, e_i)| pow_i(i, &u.betas) * e_i) + .sum(); + if u.e == r { + Ok(()) + } else { + Err(Error::NotSatisfied) + } + } + + fn sample( + &self, + _params: &CS::ProverParams, + _rng: impl RngCore, + ) -> Result<(Witness, CommittedInstance), Error> + where + CS: crate::commitment::CommitmentScheme, + { + // Sampling a random pair of witness and committed instance is required + // for the zero-knowledge layer for ProtoGalaxy, which is not supported + // yet. + // Tracking issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82 + unimplemented!() + } +} diff --git a/folding-schemes/src/folding/protogalaxy/utils.rs b/folding-schemes/src/folding/protogalaxy/utils.rs index 3ac31f1..596235c 100644 --- a/folding-schemes/src/folding/protogalaxy/utils.rs +++ b/folding-schemes/src/folding/protogalaxy/utils.rs @@ -1,5 +1,6 @@ use ark_ff::PrimeField; use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use num_integer::Integer; /// Returns (b, b^2, b^4, ..., b^{2^{t-1}}) pub fn exponential_powers(b: F, t: usize) -> Vec { @@ -70,6 +71,40 @@ pub fn betas_star_var( .collect::>>() } +/// Returns the product of selected elements in `betas`. +/// For every index `j`, whether `betas[j]` is selected is determined by the +/// `j`-th bit in the binary (little endian) representation of `i`. +/// +/// If `betas = (β, β^2, β^4, ..., β^{2^{t-1}})`, then the result is equal to +/// `β^i`. +pub fn pow_i(mut i: usize, betas: &[F]) -> F { + let mut j = 0; + let mut r = F::one(); + while i > 0 { + if i.is_odd() { + r *= betas[j]; + } + i >>= 1; + j += 1; + } + r +} + +/// The in-circuit version of `pow_i` +#[allow(dead_code)] // Will remove this once we have the decider circuit for Protogalaxy +pub fn pow_i_var(mut i: usize, betas: &[FpVar]) -> FpVar { + let mut j = 0; + let mut r = FieldVar::one(); + while i > 0 { + if i.is_odd() { + r *= &betas[j]; + } + i >>= 1; + j += 1; + } + r +} + #[cfg(test)] mod tests { use std::error::Error; @@ -78,6 +113,7 @@ mod tests { use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; use ark_relations::r1cs::ConstraintSystem; use ark_std::{test_rng, UniformRand}; + use rand::Rng; use super::*; @@ -144,4 +180,25 @@ mod tests { Ok(()) } + + #[test] + fn test_pow_i() -> Result<(), Box> { + let rng = &mut test_rng(); + + for t in 1..10 { + let cs = ConstraintSystem::::new_ref(); + + let betas = (0..t).map(|_| Fr::rand(rng)).collect::>(); + let i = rng.gen_range(0..(1 << t)); + + let betas_var = Vec::new_witness(cs.clone(), || Ok(betas.clone()))?; + + let r = pow_i(i, &betas); + let r_var = pow_i_var(i, &betas_var); + assert_eq!(r, r_var.value()?); + assert!(cs.is_satisfied()?); + } + + Ok(()) + } } diff --git a/folding-schemes/src/frontend/circom/mod.rs b/folding-schemes/src/frontend/circom/mod.rs index 11d900e..a261f72 100644 --- a/folding-schemes/src/frontend/circom/mod.rs +++ b/folding-schemes/src/frontend/circom/mod.rs @@ -258,7 +258,7 @@ pub mod tests { // Allocates z_i1 by using step_native function. let z_i = vec![Fr::from(3_u32)]; - let wrapper_circuit = crate::frontend::tests::WrapperCircuit { + let wrapper_circuit = crate::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()), @@ -367,7 +367,7 @@ pub mod tests { // Allocates z_i1 by using step_native function. let z_i = vec![Fr::from(3_u32)]; - let wrapper_circuit = crate::frontend::tests::WrapperCircuit { + let wrapper_circuit = crate::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()), diff --git a/folding-schemes/src/frontend/mod.rs b/folding-schemes/src/frontend/mod.rs index f1f73d3..7cf9e43 100644 --- a/folding-schemes/src/frontend/mod.rs +++ b/folding-schemes/src/frontend/mod.rs @@ -7,6 +7,7 @@ use ark_std::fmt::Debug; pub mod circom; pub mod noir; pub mod noname; +pub mod utils; /// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie. /// inside the agmented F' function). @@ -53,131 +54,9 @@ pub trait FCircuit: Clone + Debug { pub mod tests { use super::*; use ark_bn254::Fr; - use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget}; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; - use core::marker::PhantomData; - /// CubicFCircuit is a struct that implements the FCircuit trait, for the R1CS example circuit - /// from https://www.vitalik.ca/general/2016/12/10/qap.html, which checks `x^3 + x + 5 = y`. - /// `z_i` is used as `x`, and `z_{i+1}` is used as `y`, and at the next step, `z_{i+1}` will be - /// assigned to `z_i`, and a new `z+{i+1}` will be computted. - #[derive(Clone, Copy, Debug)] - pub struct CubicFCircuit { - _f: PhantomData, - } - impl FCircuit for CubicFCircuit { - type Params = (); - fn new(_params: Self::Params) -> Result { - Ok(Self { _f: PhantomData }) - } - fn state_len(&self) -> usize { - 1 - } - fn external_inputs_len(&self) -> usize { - 0 - } - fn step_native( - &self, - _i: usize, - z_i: Vec, - _external_inputs: Vec, - ) -> Result, Error> { - Ok(vec![z_i[0] * z_i[0] * z_i[0] + z_i[0] + F::from(5_u32)]) - } - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Vec>, - ) -> Result>, SynthesisError> { - let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; - let z_i = z_i[0].clone(); - - Ok(vec![&z_i * &z_i * &z_i + &z_i + &five]) - } - } - - /// CustomFCircuit is a circuit that has the number of constraints specified in the - /// `n_constraints` parameter. Note that the generated circuit will have very sparse matrices. - #[derive(Clone, Copy, Debug)] - pub struct CustomFCircuit { - _f: PhantomData, - pub n_constraints: usize, - } - impl FCircuit for CustomFCircuit { - type Params = usize; - - fn new(params: Self::Params) -> Result { - Ok(Self { - _f: PhantomData, - n_constraints: params, - }) - } - fn state_len(&self) -> usize { - 1 - } - fn external_inputs_len(&self) -> usize { - 0 - } - fn step_native( - &self, - _i: usize, - z_i: Vec, - _external_inputs: Vec, - ) -> Result, Error> { - let mut z_i1 = F::one(); - for _ in 0..self.n_constraints - 1 { - z_i1 *= z_i[0]; - } - Ok(vec![z_i1]) - } - fn generate_step_constraints( - &self, - cs: ConstraintSystemRef, - _i: usize, - z_i: Vec>, - _external_inputs: Vec>, - ) -> Result>, SynthesisError> { - let mut z_i1 = FpVar::::new_witness(cs.clone(), || Ok(F::one()))?; - for _ in 0..self.n_constraints - 1 { - z_i1 *= z_i[0].clone(); - } - - Ok(vec![z_i1]) - } - } - - /// WrapperCircuit is a circuit that wraps any circuit that implements the FCircuit trait. This - /// is used to test the `FCircuit.generate_step_constraints` method. This is a similar wrapping - /// than the one done in the `AugmentedFCircuit`, but without adding all the extra constraints - /// of the AugmentedF circuit logic, in order to run lighter tests when we're not interested in - /// the the AugmentedF logic but in the wrapping of the circuits. - pub struct WrapperCircuit> { - pub FC: FC, // F circuit - pub z_i: Option>, - pub z_i1: Option>, - } - impl ConstraintSynthesizer for WrapperCircuit - where - F: PrimeField, - FC: FCircuit, - { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let z_i = Vec::>::new_witness(cs.clone(), || { - Ok(self.z_i.unwrap_or(vec![F::zero()])) - })?; - let z_i1 = Vec::>::new_input(cs.clone(), || { - Ok(self.z_i1.unwrap_or(vec![F::zero()])) - })?; - let computed_z_i1 = - self.FC - .generate_step_constraints(cs.clone(), 0, z_i.clone(), vec![])?; - - computed_z_i1.enforce_equal(&z_i1)?; - Ok(()) - } - } + use utils::{CubicFCircuit, CustomFCircuit, WrapperCircuit}; #[test] fn test_testfcircuit() { diff --git a/folding-schemes/src/frontend/utils.rs b/folding-schemes/src/frontend/utils.rs new file mode 100644 index 0000000..64d8847 --- /dev/null +++ b/folding-schemes/src/frontend/utils.rs @@ -0,0 +1,181 @@ +use ark_ff::PrimeField; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar}; +use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; +#[cfg(test)] +use ark_std::marker::PhantomData; +use ark_std::{fmt::Debug, Zero}; + +use super::FCircuit; +use crate::Error; + +/// DummyCircuit is a circuit that has dummy state and external inputs whose +/// lengths are specified in the `state_len` and `external_inputs_len` +/// parameters, without any constraints. +#[derive(Clone, Debug)] +pub struct DummyCircuit { + state_len: usize, + external_inputs_len: usize, +} +impl FCircuit for DummyCircuit { + type Params = (usize, usize); + + fn new((state_len, external_inputs_len): Self::Params) -> Result { + Ok(Self { + 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, + ) -> Result, Error> { + Ok(vec![F::zero(); self.state_len]) + } + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef, + _i: usize, + _z_i: Vec>, + _external_inputs: Vec>, + ) -> Result>, SynthesisError> { + Vec::new_witness(cs.clone(), || Ok(vec![Zero::zero(); self.state_len])) + } +} + +/// CubicFCircuit is a struct that implements the FCircuit trait, for the R1CS example circuit +/// from https://www.vitalik.ca/general/2016/12/10/qap.html, which checks `x^3 + x + 5 = y`. +/// `z_i` is used as `x`, and `z_{i+1}` is used as `y`, and at the next step, `z_{i+1}` will be +/// assigned to `z_i`, and a new `z+{i+1}` will be computted. +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +pub struct CubicFCircuit { + _f: PhantomData, +} + +#[cfg(test)] +impl FCircuit for CubicFCircuit { + type Params = (); + fn new(_params: Self::Params) -> Result { + Ok(Self { _f: PhantomData }) + } + fn state_len(&self) -> usize { + 1 + } + fn external_inputs_len(&self) -> usize { + 0 + } + fn step_native( + &self, + _i: usize, + z_i: Vec, + _external_inputs: Vec, + ) -> Result, Error> { + Ok(vec![z_i[0] * z_i[0] * z_i[0] + z_i[0] + F::from(5_u32)]) + } + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef, + _i: usize, + z_i: Vec>, + _external_inputs: Vec>, + ) -> Result>, SynthesisError> { + let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; + let z_i = z_i[0].clone(); + + Ok(vec![&z_i * &z_i * &z_i + &z_i + &five]) + } +} + +/// CustomFCircuit is a circuit that has the number of constraints specified in the +/// `n_constraints` parameter. Note that the generated circuit will have very sparse matrices. +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +pub struct CustomFCircuit { + _f: PhantomData, + pub n_constraints: usize, +} + +#[cfg(test)] +impl FCircuit for CustomFCircuit { + type Params = usize; + + fn new(params: Self::Params) -> Result { + Ok(Self { + _f: PhantomData, + n_constraints: params, + }) + } + fn state_len(&self) -> usize { + 1 + } + fn external_inputs_len(&self) -> usize { + 0 + } + fn step_native( + &self, + _i: usize, + z_i: Vec, + _external_inputs: Vec, + ) -> Result, Error> { + let mut z_i1 = F::one(); + for _ in 0..self.n_constraints - 1 { + z_i1 *= z_i[0]; + } + Ok(vec![z_i1]) + } + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef, + _i: usize, + z_i: Vec>, + _external_inputs: Vec>, + ) -> Result>, SynthesisError> { + let mut z_i1 = FpVar::::new_witness(cs.clone(), || Ok(F::one()))?; + for _ in 0..self.n_constraints - 1 { + z_i1 *= z_i[0].clone(); + } + + Ok(vec![z_i1]) + } +} + +/// WrapperCircuit is a circuit that wraps any circuit that implements the FCircuit trait. This +/// is used to test the `FCircuit.generate_step_constraints` method. This is a similar wrapping +/// than the one done in the `AugmentedFCircuit`, but without adding all the extra constraints +/// of the AugmentedF circuit logic, in order to run lighter tests when we're not interested in +/// the the AugmentedF logic but in the wrapping of the circuits. +#[cfg(test)] +pub struct WrapperCircuit> { + pub FC: FC, // F circuit + pub z_i: Option>, + pub z_i1: Option>, +} + +#[cfg(test)] +impl ark_relations::r1cs::ConstraintSynthesizer for WrapperCircuit +where + F: PrimeField, + FC: FCircuit, +{ + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + let z_i = + Vec::>::new_witness(cs.clone(), || Ok(self.z_i.unwrap_or(vec![F::zero()])))?; + let z_i1 = + Vec::>::new_input(cs.clone(), || Ok(self.z_i1.unwrap_or(vec![F::zero()])))?; + let computed_z_i1 = + self.FC + .generate_step_constraints(cs.clone(), 0, z_i.clone(), vec![])?; + + use ark_r1cs_std::eq::EqGadget; + computed_z_i1.enforce_equal(&z_i1)?; + Ok(()) + } +} diff --git a/folding-schemes/src/utils/vec.rs b/folding-schemes/src/utils/vec.rs index 3ac9203..5cf692e 100644 --- a/folding-schemes/src/utils/vec.rs +++ b/folding-schemes/src/utils/vec.rs @@ -82,7 +82,7 @@ pub fn vec_add(a: &[F], b: &[F]) -> Result, Error> { b.len(), )); } - Ok(a.iter().zip(b.iter()).map(|(x, y)| *x + y).collect()) + Ok(cfg_iter!(a).zip(b).map(|(x, y)| *x + y).collect()) } pub fn vec_sub(a: &[F], b: &[F]) -> Result, Error> { @@ -94,15 +94,15 @@ pub fn vec_sub(a: &[F], b: &[F]) -> Result, Error> { b.len(), )); } - Ok(a.iter().zip(b.iter()).map(|(x, y)| *x - y).collect()) + Ok(cfg_iter!(a).zip(b).map(|(x, y)| *x - y).collect()) } pub fn vec_scalar_mul(vec: &[F], c: &F) -> Vec { - vec.iter().map(|a| *a * c).collect() + cfg_iter!(vec).map(|a| *a * c).collect() } pub fn is_zero_vec(vec: &[F]) -> bool { - vec.iter().all(|a| a.is_zero()) + cfg_iter!(vec).all(|a| a.is_zero()) } pub fn mat_vec_mul_dense(M: &[Vec], z: &[F]) -> Result, Error> { @@ -118,13 +118,9 @@ pub fn mat_vec_mul_dense(M: &[Vec], z: &[F]) -> Result, )); } - let mut r: Vec = vec![F::zero(); M.len()]; - for (i, M_i) in M.iter().enumerate() { - for (j, M_ij) in M_i.iter().enumerate() { - r[i] += *M_ij * z[j]; - } - } - Ok(r) + Ok(cfg_iter!(M) + .map(|row| row.iter().zip(z).map(|(a, b)| *a * b).sum()) + .collect()) } pub fn mat_vec_mul(M: &SparseMatrix, z: &[F]) -> Result, Error> { @@ -136,13 +132,9 @@ pub fn mat_vec_mul(M: &SparseMatrix, z: &[F]) -> Result z.len(), )); } - let mut res = vec![F::zero(); M.n_rows]; - for (row_i, row) in M.coeffs.iter().enumerate() { - for &(value, col_i) in row.iter() { - res[row_i] += value * z[col_i]; - } - } - Ok(res) + Ok(cfg_iter!(M.coeffs) + .map(|row| row.iter().map(|(value, col_i)| *value * z[*col_i]).sum()) + .collect()) } pub fn mat_from_str_mat(str_mat: Vec>) -> Result>, Error> { From 1947ab0f51ec535a337b1c41bb399d56a0ec6954 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 19 Sep 2024 10:04:10 +0200 Subject: [PATCH 5/5] feat/hypernova serialization (#159) * feat: implement serialization of hypernova prover and verifier params, proof and public inputs * chore: remove leftover comments * chore: remove timers * chore: improve typing and import naming --- folding-schemes/src/folding/hypernova/cccs.rs | 4 +- .../src/folding/hypernova/decider_eth.rs | 220 ++++++++- .../src/folding/hypernova/lcccs.rs | 4 +- folding-schemes/src/folding/hypernova/mod.rs | 34 +- .../src/folding/hypernova/serialize.rs | 420 ++++++++++++++++++ 5 files changed, 656 insertions(+), 26 deletions(-) create mode 100644 folding-schemes/src/folding/hypernova/serialize.rs diff --git a/folding-schemes/src/folding/hypernova/cccs.rs b/folding-schemes/src/folding/hypernova/cccs.rs index de5c281..5b95df2 100644 --- a/folding-schemes/src/folding/hypernova/cccs.rs +++ b/folding-schemes/src/folding/hypernova/cccs.rs @@ -1,6 +1,8 @@ use ark_crypto_primitives::sponge::Absorb; use ark_ec::CurveGroup; use ark_ff::PrimeField; +use ark_serialize::CanonicalDeserialize; +use ark_serialize::CanonicalSerialize; use ark_std::One; use ark_std::Zero; use std::sync::Arc; @@ -17,7 +19,7 @@ use crate::utils::virtual_polynomial::{build_eq_x_r_vec, VirtualPolynomial}; use crate::Error; /// Committed CCS instance -#[derive(Debug, Clone)] +#[derive(Debug, Clone, CanonicalSerialize, CanonicalDeserialize)] pub struct CCCS { // Commitment to witness pub C: C, diff --git a/folding-schemes/src/folding/hypernova/decider_eth.rs b/folding-schemes/src/folding/hypernova/decider_eth.rs index 3e659fe..e45876e 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth.rs @@ -3,6 +3,7 @@ use ark_crypto_primitives::sponge::Absorb; use ark_ec::{CurveGroup, Group}; use ark_ff::PrimeField; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_snark::SNARK; use ark_std::rand::{CryptoRng, RngCore}; use ark_std::{One, Zero}; @@ -14,11 +15,12 @@ use crate::commitment::{ kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme, }; use crate::folding::circuits::{nonnative::affine::NonNativeAffineVar, CF2}; +use crate::folding::nova::decider_eth::VerifierParam; use crate::frontend::FCircuit; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Proof where C1: CurveGroup, @@ -85,8 +87,7 @@ where type PreprocessorParam = (FS::ProverParam, FS::VerifierParam); type ProverParam = (S::ProvingKey, CS1::ProverParams); type Proof = Proof; - /// VerifierParam = (pp_hash, snark::vk, commitment_scheme::vk) - type VerifierParam = (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams); + type VerifierParam = VerifierParam; type PublicInput = Vec; type CommittedInstance = (); @@ -120,7 +121,12 @@ where let pp_hash = hypernova_vp.pp_hash()?; let pp = (g16_pk, hypernova_pp.cs_pp); - let vp = (pp_hash, g16_vk, hypernova_vp.cs_vp); + + let vp = VerifierParam { + pp_hash, + snark_vp: g16_vk, + cs_vp: hypernova_vp.cs_vp, + }; Ok((pp, vp)) } @@ -183,7 +189,7 @@ where } let (pp_hash, snark_vk, cs_vk): (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams) = - vp; + (vp.pp_hash, vp.snark_vp, vp.cs_vp); // Note: the NIMFS proof is checked inside the DeciderEthCircuit, which ensures that the // 'proof.U_i1' is correctly computed @@ -223,11 +229,15 @@ pub mod tests { use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective}; use ark_groth16::Groth16; use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; - use std::time::Instant; + use ark_serialize::{Compress, Validate}; use super::*; use crate::commitment::{kzg::KZG, pedersen::Pedersen}; - use crate::folding::hypernova::PreprocessorParam; + use crate::folding::hypernova::cccs::CCCS; + use crate::folding::hypernova::{ + PreprocessorParam, ProverParams, VerifierParams as HyperNovaVerifierParams, + }; + use crate::folding::nova::decider_eth::VerifierParam; use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; @@ -262,7 +272,7 @@ pub mod tests { NU, >; - let mut rng = ark_std::test_rng(); + let mut rng = rand::rngs::OsRng; let poseidon_config = poseidon_canonical_config::(); let F_circuit = CubicFCircuit::::new(()).unwrap(); @@ -271,31 +281,22 @@ pub mod tests { let prep_param = PreprocessorParam::new(poseidon_config, F_circuit); let hypernova_params = HN::preprocess(&mut rng, &prep_param).unwrap(); - let start = Instant::now(); let mut hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone()).unwrap(); - println!("Nova initialized, {:?}", start.elapsed()); - let start = Instant::now(); hypernova .prove_step(&mut rng, vec![], Some((vec![], vec![]))) .unwrap(); - println!("prove_step, {:?}", start.elapsed()); hypernova .prove_step(&mut rng, vec![], Some((vec![], vec![]))) .unwrap(); // do a 2nd step - let mut rng = rand::rngs::OsRng; - // prepare the Decider prover & verifier params let (decider_pp, decider_vp) = D::preprocess(&mut rng, hypernova_params, hypernova.clone()).unwrap(); // decider proof generation - let start = Instant::now(); let proof = D::prove(rng, decider_pp, hypernova.clone()).unwrap(); - println!("Decider prove, {:?}", start.elapsed()); // decider proof verification - let start = Instant::now(); let verified = D::verify( decider_vp, hypernova.i, @@ -307,6 +308,189 @@ pub mod tests { ) .unwrap(); assert!(verified); - println!("Decider verify, {:?}", start.elapsed()); + } + + #[test] + fn test_decider_serialization() { + const MU: usize = 1; + const NU: usize = 1; + // use HyperNova as FoldingScheme + type HN = HyperNova< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + KZG<'static, Bn254>, + Pedersen, + MU, + NU, + false, + >; + type D = Decider< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + KZG<'static, Bn254>, + Pedersen, + Groth16, // here we define the Snark to use in the decider + HN, // here we define the FoldingScheme to use + MU, + NU, + >; + + let mut rng = ark_std::test_rng(); + let poseidon_config = poseidon_canonical_config::(); + + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let z_0 = vec![Fr::from(3_u32)]; + + let prep_param = PreprocessorParam::new(poseidon_config.clone(), F_circuit); + let hypernova_params = HN::preprocess(&mut rng, &prep_param).unwrap(); + + let hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone()).unwrap(); + + let mut rng = rand::rngs::OsRng; + + // prepare the Decider prover & verifier params + let (decider_pp, decider_vp) = + D::preprocess(&mut rng, hypernova_params.clone(), hypernova.clone()).unwrap(); + + let mut hypernova_pp_serialized = vec![]; + hypernova_params + .0 + .clone() + .serialize_compressed(&mut hypernova_pp_serialized) + .unwrap(); + let mut hypernova_vp_serialized = vec![]; + hypernova_params + .1 + .clone() + .serialize_compressed(&mut hypernova_vp_serialized) + .unwrap(); + + let hypernova_pp_deserialized = ProverParams::< + Projective, + Projective2, + KZG<'static, Bn254>, + Pedersen, + false, + >::deserialize_prover_params( + hypernova_pp_serialized.as_slice(), + Compress::Yes, + Validate::No, + &hypernova_params.0.ccs, + &poseidon_config, + ) + .unwrap(); + + let hypernova_vp_deserialized = HyperNovaVerifierParams::< + Projective, + Projective2, + KZG<'static, Bn254>, + Pedersen, + false, + >::deserialize_verifier_params( + hypernova_vp_serialized.as_slice(), + Compress::Yes, + Validate::No, + &hypernova_params.0.ccs.unwrap(), + &poseidon_config, + ) + .unwrap(); + + let hypernova_params = (hypernova_pp_deserialized, hypernova_vp_deserialized); + let mut hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone()).unwrap(); + + hypernova + .prove_step(&mut rng, vec![], Some((vec![], vec![]))) + .unwrap(); + hypernova + .prove_step(&mut rng, vec![], Some((vec![], vec![]))) + .unwrap(); + + // decider proof generation + let proof = D::prove(rng, decider_pp, hypernova.clone()).unwrap(); + + let verified = D::verify( + decider_vp.clone(), + hypernova.i.clone(), + hypernova.z_0.clone(), + hypernova.z_i.clone(), + &(), + &(), + &proof, + ) + .unwrap(); + assert!(verified); + + // The rest of this test will serialize the data and deserialize it back, and use it to + // verify the proof: + + // serialize the verifier_params, proof and public inputs + let mut decider_vp_serialized = vec![]; + decider_vp + .serialize_compressed(&mut decider_vp_serialized) + .unwrap(); + let mut proof_serialized = vec![]; + proof.serialize_compressed(&mut proof_serialized).unwrap(); + // serialize the public inputs in a single packet + let mut public_inputs_serialized = vec![]; + hypernova + .i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + hypernova + .z_0 + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + hypernova + .z_i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + hypernova + .U_i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + hypernova + .u_i + .serialize_compressed(&mut public_inputs_serialized) + .unwrap(); + + // deserialize back the verifier_params, proof and public inputs + let decider_vp_deserialized = + VerifierParam::< + Projective, + as CommitmentScheme>::VerifierParams, + as SNARK>::VerifyingKey, + >::deserialize_compressed(&mut decider_vp_serialized.as_slice()) + .unwrap(); + + let proof_deserialized = + Proof::, Groth16>::deserialize_compressed( + &mut proof_serialized.as_slice(), + ) + .unwrap(); + + let mut reader = public_inputs_serialized.as_slice(); + let i_deserialized = Fr::deserialize_compressed(&mut reader).unwrap(); + let z_0_deserialized = Vec::::deserialize_compressed(&mut reader).unwrap(); + let z_i_deserialized = Vec::::deserialize_compressed(&mut reader).unwrap(); + let _U_i = LCCCS::::deserialize_compressed(&mut reader).unwrap(); + let _u_i = CCCS::::deserialize_compressed(&mut reader).unwrap(); + + let verified = D::verify( + decider_vp_deserialized, + i_deserialized.clone(), + z_0_deserialized.clone(), + z_i_deserialized.clone(), + &(), + &(), + &proof_deserialized, + ) + .unwrap(); + assert!(verified); } } diff --git a/folding-schemes/src/folding/hypernova/lcccs.rs b/folding-schemes/src/folding/hypernova/lcccs.rs index 8cca4bc..6aa1652 100644 --- a/folding-schemes/src/folding/hypernova/lcccs.rs +++ b/folding-schemes/src/folding/hypernova/lcccs.rs @@ -3,6 +3,8 @@ use ark_ec::{CurveGroup, Group}; use ark_ff::PrimeField; use ark_poly::DenseMultilinearExtension; use ark_poly::MultilinearExtension; +use ark_serialize::CanonicalDeserialize; +use ark_serialize::CanonicalSerialize; use ark_std::rand::Rng; use ark_std::Zero; @@ -15,7 +17,7 @@ use crate::utils::vec::mat_vec_mul; use crate::Error; /// Linearized Committed CCS instance -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct LCCCS { // Commitment to witness pub C: C, diff --git a/folding-schemes/src/folding/hypernova/mod.rs b/folding-schemes/src/folding/hypernova/mod.rs index f3c90ea..2917ea4 100644 --- a/folding-schemes/src/folding/hypernova/mod.rs +++ b/folding-schemes/src/folding/hypernova/mod.rs @@ -6,6 +6,7 @@ use ark_crypto_primitives::sponge::{ use ark_ec::{CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{fmt::Debug, marker::PhantomData, rand::RngCore, One, Zero}; pub mod cccs; @@ -14,6 +15,7 @@ pub mod decider_eth; pub mod decider_eth_circuit; pub mod lcccs; pub mod nimfs; +pub mod serialize; pub mod utils; use cccs::CCCS; @@ -62,7 +64,7 @@ pub type HyperNovaCycleFoldCircuit = CycleFoldCircuit, GC>; /// Witness for the LCCCS & CCCS, containing the w vector, and the r_w used as randomness in the Pedersen commitment. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Witness { pub w: Vec, pub r_w: F, @@ -939,13 +941,25 @@ mod tests { } // test_ivc allowing to choose the CommitmentSchemes - fn test_ivc_opt< + pub fn test_ivc_opt< CS1: CommitmentScheme, CS2: CommitmentScheme, const H: bool, >( poseidon_config: PoseidonConfig, F_circuit: CubicFCircuit, + ) -> ( + HyperNova, CS1, CS2, 2, 3, H>, + ( + ProverParams, + VerifierParams, + ), + (LCCCS, Witness), + (CCCS, Witness), + ( + CycleFoldCommittedInstance, + CycleFoldWitness, + ), ) { let mut rng = ark_std::test_rng(); @@ -1001,14 +1015,22 @@ mod tests { let (running_instance, incoming_instance, cyclefold_instance) = hypernova.instances(); HN::verify( - hypernova_params.1, // verifier_params + hypernova_params.1.clone(), // verifier_params z_0, - hypernova.z_i, - hypernova.i, + hypernova.z_i.clone(), + hypernova.i.clone(), + running_instance.clone(), + incoming_instance.clone(), + cyclefold_instance.clone(), + ) + .unwrap(); + + ( + hypernova, + hypernova_params, running_instance, incoming_instance, cyclefold_instance, ) - .unwrap(); } } diff --git a/folding-schemes/src/folding/hypernova/serialize.rs b/folding-schemes/src/folding/hypernova/serialize.rs new file mode 100644 index 0000000..a7aa6d0 --- /dev/null +++ b/folding-schemes/src/folding/hypernova/serialize.rs @@ -0,0 +1,420 @@ +use crate::arith::ccs::CCS; +use crate::arith::r1cs::R1CS; +use crate::folding::hypernova::ProverParams; +use crate::folding::hypernova::VerifierParams; +use ark_crypto_primitives::sponge::poseidon::PoseidonConfig; +use ark_crypto_primitives::sponge::Absorb; +use ark_ec::{CurveGroup, Group}; +use ark_ff::PrimeField; +use ark_r1cs_std::groups::{CurveVar, GroupOpsBounds}; +use ark_r1cs_std::ToConstraintFieldGadget; +use ark_serialize::CanonicalDeserialize; +use ark_serialize::{CanonicalSerialize, Compress, SerializationError, Validate}; +use ark_std::marker::PhantomData; + +use crate::folding::hypernova::cccs::CCCS; +use crate::folding::hypernova::lcccs::LCCCS; +use crate::folding::hypernova::Witness; +use crate::folding::nova::{ + CommittedInstance as CycleFoldCommittedInstance, Witness as CycleFoldWitness, +}; +use crate::FoldingScheme; +use crate::{ + commitment::CommitmentScheme, + folding::{circuits::CF2, nova::PreprocessorParam}, + frontend::FCircuit, +}; + +use super::HyperNova; + +impl + CanonicalSerialize for HyperNova +where + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar>, + FC: FCircuit, + CS1: CommitmentScheme, + CS2: CommitmentScheme, +{ + fn serialize_compressed( + &self, + writer: W, + ) -> Result<(), ark_serialize::SerializationError> { + self.serialize_with_mode(writer, ark_serialize::Compress::Yes) + } + + fn compressed_size(&self) -> usize { + self.serialized_size(ark_serialize::Compress::Yes) + } + + fn serialize_uncompressed( + &self, + writer: W, + ) -> Result<(), ark_serialize::SerializationError> { + self.serialize_with_mode(writer, ark_serialize::Compress::No) + } + + fn uncompressed_size(&self) -> usize { + self.serialized_size(ark_serialize::Compress::No) + } + + fn serialize_with_mode( + &self, + mut writer: W, + compress: ark_serialize::Compress, + ) -> Result<(), ark_serialize::SerializationError> { + self.pp_hash.serialize_with_mode(&mut writer, compress)?; + self.i.serialize_with_mode(&mut writer, compress)?; + self.z_0.serialize_with_mode(&mut writer, compress)?; + self.z_i.serialize_with_mode(&mut writer, compress)?; + self.W_i.serialize_with_mode(&mut writer, compress)?; + self.U_i.serialize_with_mode(&mut writer, compress)?; + self.w_i.serialize_with_mode(&mut writer, compress)?; + self.u_i.serialize_with_mode(&mut writer, compress)?; + self.cf_W_i.serialize_with_mode(&mut writer, compress)?; + self.cf_U_i.serialize_with_mode(&mut writer, compress) + } + + fn serialized_size(&self, compress: ark_serialize::Compress) -> usize { + self.pp_hash.serialized_size(compress) + + self.i.serialized_size(compress) + + self.z_0.serialized_size(compress) + + self.z_i.serialized_size(compress) + + self.W_i.serialized_size(compress) + + self.U_i.serialized_size(compress) + + self.w_i.serialized_size(compress) + + self.u_i.serialized_size(compress) + + self.cf_W_i.serialized_size(compress) + + self.cf_U_i.serialized_size(compress) + } +} + +impl + HyperNova +where + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + ::BaseField: PrimeField, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + ::ScalarField: Absorb, + C1: CurveGroup, + for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + #[allow(clippy::too_many_arguments)] + pub fn deserialize_hypernova( + mut reader: R, + compress: Compress, + validate: Validate, + poseidon_config: PoseidonConfig, + cs_pp: CS1::ProverParams, + cs_vp: CS1::VerifierParams, + cf_cs_pp: CS2::ProverParams, + cf_cs_vp: CS2::VerifierParams, + ) -> Result { + let f_circuit = FC::new(()).unwrap(); + let prep_param = PreprocessorParam { + poseidon_config: poseidon_config.clone(), + F: f_circuit.clone(), + cs_pp: Some(cs_pp.clone()), + cs_vp: Some(cs_vp.clone()), + cf_cs_pp: Some(cf_cs_pp.clone()), + cf_cs_vp: Some(cf_cs_vp.clone()), + }; + // `test_rng` won't be used in `preprocess`, since parameters have already been initialized + let (prover_params, verifier_params) = Self::preprocess(ark_std::test_rng(), &prep_param) + .or(Err(SerializationError::InvalidData))?; + let pp_hash = C1::ScalarField::deserialize_with_mode(&mut reader, compress, validate)?; + let i = C1::ScalarField::deserialize_with_mode(&mut reader, compress, validate)?; + let z_0 = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + let z_i = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + let W_i = + Witness::::deserialize_with_mode(&mut reader, compress, validate)?; + let U_i = LCCCS::::deserialize_with_mode(&mut reader, compress, validate)?; + let w_i = + Witness::::deserialize_with_mode(&mut reader, compress, validate)?; + let u_i = CCCS::::deserialize_with_mode(&mut reader, compress, validate)?; + let cf_W_i = + CycleFoldWitness::::deserialize_with_mode(&mut reader, compress, validate)?; + let cf_U_i = CycleFoldCommittedInstance::::deserialize_with_mode( + &mut reader, + compress, + validate, + )?; + let ccs = prover_params.ccs.ok_or(SerializationError::InvalidData)?; + + Ok(HyperNova { + _gc1: PhantomData, + _c2: PhantomData, + _gc2: PhantomData, + ccs, + cf_r1cs: verifier_params.cf_r1cs, + poseidon_config, + cs_pp, + cf_cs_pp, + F: f_circuit, + pp_hash, + i, + z_0, + z_i, + W_i, + U_i, + w_i, + u_i, + cf_W_i, + cf_U_i, + }) + } +} + +impl< + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + const H: bool, + > CanonicalSerialize for ProverParams +{ + fn serialize_compressed( + &self, + writer: W, + ) -> Result<(), SerializationError> { + self.serialize_with_mode(writer, Compress::Yes) + } + + fn compressed_size(&self) -> usize { + self.serialized_size(Compress::Yes) + } + + fn serialize_uncompressed( + &self, + writer: W, + ) -> Result<(), SerializationError> { + self.serialize_with_mode(writer, Compress::No) + } + + fn uncompressed_size(&self) -> usize { + self.serialized_size(Compress::No) + } + + fn serialize_with_mode( + &self, + mut writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + self.cs_pp.serialize_with_mode(&mut writer, compress)?; + self.cf_cs_pp.serialize_with_mode(&mut writer, compress) + } + + fn serialized_size(&self, compress: Compress) -> usize { + self.cs_pp.serialized_size(compress) + self.cf_cs_pp.serialized_size(compress) + } +} + +impl< + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + const H: bool, + > ProverParams +{ + pub fn deserialize_prover_params( + mut reader: R, + compress: Compress, + validate: Validate, + ccs: &Option>, + poseidon_config: &PoseidonConfig, + ) -> Result { + let cs_pp = CS1::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; + let cf_cs_pp = CS2::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?; + + Ok(ProverParams { + cs_pp, + cf_cs_pp, + ccs: ccs.clone(), + poseidon_config: poseidon_config.clone(), + }) + } +} + +impl< + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + const H: bool, + > CanonicalSerialize for VerifierParams +{ + fn serialize_compressed( + &self, + writer: W, + ) -> Result<(), SerializationError> { + self.serialize_with_mode(writer, Compress::Yes) + } + + fn compressed_size(&self) -> usize { + self.serialized_size(Compress::Yes) + } + + fn serialize_uncompressed( + &self, + writer: W, + ) -> Result<(), SerializationError> { + self.serialize_with_mode(writer, Compress::No) + } + + fn uncompressed_size(&self) -> usize { + self.serialized_size(Compress::No) + } + + fn serialize_with_mode( + &self, + mut writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + self.cf_r1cs.serialize_with_mode(&mut writer, compress)?; + self.cs_vp.serialize_with_mode(&mut writer, compress)?; + self.cf_cs_vp.serialize_with_mode(&mut writer, compress) + } + + fn serialized_size(&self, compress: Compress) -> usize { + self.cf_r1cs.serialized_size(compress) + + self.cs_vp.serialized_size(compress) + + self.cf_cs_vp.serialized_size(compress) + } +} + +impl< + C1: CurveGroup, + C2: CurveGroup, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + const H: bool, + > VerifierParams +{ + pub fn deserialize_verifier_params( + mut reader: R, + compress: Compress, + validate: Validate, + ccs: &CCS, + poseidon_config: &PoseidonConfig, + ) -> Result { + let cf_r1cs = R1CS::deserialize_with_mode(&mut reader, compress, validate)?; + let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; + let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; + Ok(VerifierParams { + ccs: ccs.clone(), + poseidon_config: poseidon_config.clone(), + cf_r1cs, + cs_vp, + cf_cs_vp, + }) + } +} + +#[cfg(test)] +pub mod tests { + use crate::FoldingScheme; + use crate::MultiFolding; + use ark_serialize::{Compress, Validate, Write}; + use std::fs; + + use crate::{ + commitment::{kzg::KZG, pedersen::Pedersen}, + folding::hypernova::{tests::test_ivc_opt, HyperNova}, + frontend::{utils::CubicFCircuit, FCircuit}, + transcript::poseidon::poseidon_canonical_config, + }; + use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective}; + use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; + use ark_serialize::CanonicalSerialize; + + #[test] + fn test_serde_hypernova() { + let poseidon_config = poseidon_canonical_config::(); + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let (mut hn, (_, verifier_params), _, _, _) = test_ivc_opt::< + KZG, + Pedersen, + false, + >(poseidon_config.clone(), F_circuit); + + let mut writer = vec![]; + assert!(hn.serialize_compressed(&mut writer).is_ok()); + let mut writer = vec![]; + assert!(hn.serialize_uncompressed(&mut writer).is_ok()); + + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .open("./hypernova.serde") + .unwrap(); + + file.write_all(&writer).unwrap(); + + let bytes = fs::read("./hypernova.serde").unwrap(); + + let mut hn_deserialized = HyperNova::< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + KZG, + Pedersen, + 2, + 3, + false, + >::deserialize_hypernova( + bytes.as_slice(), + Compress::No, + Validate::No, + poseidon_config, + hn.cs_pp.clone(), + verifier_params.cs_vp, + hn.cf_cs_pp.clone(), + verifier_params.cf_cs_vp, + ) + .unwrap(); + + assert_eq!(hn.i, hn_deserialized.i); + + let mut rng = ark_std::test_rng(); + for _ in 0..3 { + // prepare some new instances to fold in the multifolding step + let mut lcccs = vec![]; + for j in 0..1 { + let instance_state = vec![Fr::from(j as u32 + 85_u32)]; + let (U, W) = hn + .new_running_instance(&mut rng, instance_state, vec![]) + .unwrap(); + lcccs.push((U, W)); + } + let mut cccs = vec![]; + for j in 0..2 { + let instance_state = vec![Fr::from(j as u32 + 15_u32)]; + let (u, w) = hn + .new_incoming_instance(&mut rng, instance_state, vec![]) + .unwrap(); + cccs.push((u, w)); + } + + hn.prove_step(&mut rng, vec![], Some((lcccs.clone(), cccs.clone()))) + .unwrap(); + hn_deserialized + .prove_step(&mut rng, vec![], Some((lcccs, cccs))) + .unwrap(); + } + + assert_eq!(hn.z_i, hn_deserialized.z_i); + } +}