From 4fd44723a55512ff09a5d5002e996c1c42aa0d6f Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 04:39:50 +0800 Subject: [PATCH 01/93] Refactor: port sumcheck, circuit traits, and more --- Cargo.toml | 2 - crates/primitives/Cargo.toml | 5 +- .../src/arithmetizations/ccs/circuits.rs | 18 +- .../src/arithmetizations/ccs/mod.rs | 73 ++-- crates/primitives/src/arithmetizations/mod.rs | 133 +----- .../src/arithmetizations/r1cs/circuits.rs | 14 +- .../src/arithmetizations/r1cs/mod.rs | 282 +++++++++---- crates/primitives/src/circuits/mod.rs | 161 +++++++ crates/primitives/src/circuits/utils.rs | 80 ++++ crates/primitives/src/commitments/mod.rs | 47 ++- crates/primitives/src/commitments/pedersen.rs | 52 ++- .../src/gadgets/nonnative/affine.rs | 16 +- .../primitives/src/gadgets/nonnative/uint.rs | 21 +- crates/primitives/src/lib.rs | 6 +- crates/primitives/src/relations/mod.rs | 52 +++ crates/primitives/src/sumcheck/mod.rs | 291 +++++++++++++ crates/primitives/src/sumcheck/utils.rs | 399 ++++++++++++++++++ .../lib.rs => primitives/src/traits/mod.rs} | 139 ++++-- crates/primitives/src/transcripts/mod.rs | 3 +- crates/primitives/src/transcripts/poseidon.rs | 4 +- crates/traits/Cargo.toml | 21 - 21 files changed, 1437 insertions(+), 382 deletions(-) create mode 100644 crates/primitives/src/circuits/mod.rs create mode 100644 crates/primitives/src/circuits/utils.rs create mode 100644 crates/primitives/src/relations/mod.rs create mode 100644 crates/primitives/src/sumcheck/mod.rs create mode 100644 crates/primitives/src/sumcheck/utils.rs rename crates/{traits/src/lib.rs => primitives/src/traits/mod.rs} (73%) delete mode 100644 crates/traits/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index e8fe65f39..b56b4da35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ "crates/primitives", - "crates/traits", ] resolver = "2" @@ -79,4 +78,3 @@ ark-vesta = { version = "^0.5.0" } # Local crates sonobe-primitives = { path = "crates/primitives", default-features = false } -sonobe-traits = { path = "crates/traits" } \ No newline at end of file diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index feca812a6..efea822bc 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -20,17 +20,14 @@ num-integer = { workspace = true } rayon = { workspace = true } thiserror = { workspace = true } -sonobe-traits = { workspace = true } - [dev-dependencies] ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } ark-pallas = { workspace = true, features = ["curve", "r1cs"] } -ark-vesta = { workspace = true, features = ["r1cs"] } [features] default = ["parallel"] parallel = [ + "ark-poly-commit/parallel", "ark-relations/parallel", "ark-r1cs-std/parallel", - "sonobe-traits/parallel", ] \ No newline at end of file diff --git a/crates/primitives/src/arithmetizations/ccs/circuits.rs b/crates/primitives/src/arithmetizations/ccs/circuits.rs index f239d691f..60fec5c33 100644 --- a/crates/primitives/src/arithmetizations/ccs/circuits.rs +++ b/crates/primitives/src/arithmetizations/ccs/circuits.rs @@ -6,9 +6,10 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::borrow::Borrow; -use super::CCS; use crate::gadgets::math::matrix::SparseMatrixVar; +use super::CCS; + /// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. #[derive(Debug, Clone)] pub struct CCSMatricesVar { @@ -24,13 +25,14 @@ impl AllocVar, F> for CCSMatricesVar { ) -> Result { f().and_then(|val| { let cs = cs.into(); - let M: Vec>> = val - .borrow() - .M - .iter() - .map(|M| SparseMatrixVar::>::new_constant(cs.clone(), M.clone())) - .collect::>()?; - Ok(Self { M }) + Ok(Self { + M: val + .borrow() + .M + .iter() + .map(|M| SparseMatrixVar::>::new_constant(cs.clone(), M.clone())) + .collect::>()?, + }) }) } } diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 32357dd01..8418f8a4b 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -1,12 +1,13 @@ use ark_ff::Field; +use ark_poly::DenseMultilinearExtension; use ark_relations::gr1cs::Matrix; use ark_std::{cfg_into_iter, log2}; #[cfg(feature = "parallel")] use rayon::prelude::*; -use crate::arithmetizations::{Assignments, Error}; +use crate::circuits::Assignments; -use super::{r1cs::R1CS, Arith, ArithRelation, ArithSerializer}; +use super::{r1cs::R1CS, Arith, ArithRelation, Error}; pub mod circuits; @@ -15,17 +16,15 @@ pub mod circuits; #[derive(Debug, Clone, Eq, PartialEq)] pub struct CCS { /// m: number of rows in M_i (such that M_i \in F^{m, n}) - m: usize, + pub m: usize, /// n = |z|, number of cols in M_i - n: usize, + pub n: usize, /// l = |io|, size of public input/output - l: usize, + pub l: usize, /// t = |M|, number of matrices pub t: usize, - /// q = |c| = |S|, number of multisets - q: usize, /// d: max degree in each variable - d: usize, + pub d: usize, /// s = log(m), dimension of x pub s: usize, @@ -39,7 +38,23 @@ pub struct CCS { impl CCS { /// Evaluates the CCS relation at a given vector of assignments `z` - pub fn eval_at_z(&self, z: Assignments) -> Result, Error> { + pub fn eval_assignments( + &self, + z: Assignments + Sync>, + ) -> Result, Error> { + let public_len = z.public.as_ref().len(); + let private_len = z.private.as_ref().len(); + if public_len != self.n_public_inputs() { + return Err(Error::MalformedAssignments( + format!("The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", self.n_public_inputs(), public_len) + )); + } + if private_len != self.n_witnesses() { + return Err(Error::MalformedAssignments( + format!("The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", self.n_witnesses(), private_len) + )); + } + // Recall that the evaluation of CCS at z is defined as: // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i * z))$, // where $\prod$ denotes the Hadamard product. @@ -77,6 +92,21 @@ impl CCS { }) .collect()) } + + pub fn mle( + &self, + i: usize, + z: Assignments>, + ) -> DenseMultilinearExtension { + DenseMultilinearExtension { + num_vars: self.s, + evaluations: self.M[i] + .iter() + .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) + .chain(vec![F::zero(); (1 << self.s) - self.m]) + .collect(), + } + } } impl Arith for CCS { @@ -106,14 +136,14 @@ impl Arith for CCS { } } -impl, U: AsRef<[F]>> ArithRelation for CCS { +impl ArithRelation, Vec> for CCS { type Evaluation = Vec; - fn eval_relation(&self, w: &W, u: &U) -> Result { - self.eval_at_z((F::one(), u.as_ref(), w.as_ref()).into()) + fn eval_relation(&self, w: &[F], u: &[F]) -> Result { + self.eval_assignments((F::one(), u, w).into()) } - fn check_evaluation(_w: &W, _u: &U, e: Self::Evaluation) -> Result<(), Error> { + fn check_evaluation(_w: &[F], _u: &[F], e: Self::Evaluation) -> Result<(), Error> { cfg_into_iter!(e) .all(|i| i.is_zero()) .then_some(()) @@ -123,20 +153,6 @@ impl, U: AsRef<[F]>> ArithRelation for CCS { } } -impl ArithSerializer for CCS { - fn params_to_le_bytes(&self) -> Vec { - [ - self.l.to_le_bytes(), - self.m.to_le_bytes(), - self.n.to_le_bytes(), - self.t.to_le_bytes(), - self.q.to_le_bytes(), - self.d.to_le_bytes(), - ] - .concat() - } -} - impl From> for CCS { fn from(r1cs: R1CS) -> Self { let m = r1cs.n_constraints(); @@ -147,7 +163,6 @@ impl From> for CCS { l: r1cs.n_public_inputs(), s: log2(m) as usize, t: 3, - q: 2, d: r1cs.degree(), S: vec![vec![0, 1], vec![2]], @@ -157,4 +172,4 @@ impl From> for CCS { } } -// TODO: add back tests \ No newline at end of file +// TODO: add back tests diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index 24adb29e3..5567ef644 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,10 +1,8 @@ -use std::ops::Index; - use ark_relations::gr1cs::SynthesisError; -use ark_std::rand::RngCore; -use sonobe_traits::Dummy; use thiserror::Error; +use crate::relations::{Referenceable, Relation}; + pub mod ccs; pub mod r1cs; @@ -16,66 +14,8 @@ pub enum Error { UnsatisfiedAssignments(String), #[error("Failed to extract constraints from the constraint system: {0}")] ConstraintExtractionFailure(String), -} - -pub struct Assignments<'a, F> { - pub constant: F, - pub public: &'a [F], - pub private: &'a [F], -} - -impl<'a, F> From<(F, &'a [F], &'a [F])> for Assignments<'a, F> { - fn from((u, x, w): (F, &'a [F], &'a [F])) -> Self { - Self { - constant: u, - public: x, - private: w, - } - } -} - -impl<'a, F> Index for Assignments<'a, F> { - type Output = F; - - fn index(&self, index: usize) -> &Self::Output { - if index == 0 { - &self.constant - } else if index <= self.public.len() { - &self.public[index - 1] - } else { - &self.private[index - 1 - self.public.len()] - } - } -} - -pub struct AssignmentsVar<'a, FV> { - pub constant: FV, - pub public: &'a [FV], - pub private: &'a [FV], -} - -impl<'a, FV> From<(FV, &'a [FV], &'a [FV])> for AssignmentsVar<'a, FV> { - fn from((u, x, w): (FV, &'a [FV], &'a [FV])) -> Self { - Self { - constant: u, - public: x, - private: w, - } - } -} - -impl<'a, FV> Index for AssignmentsVar<'a, FV> { - type Output = FV; - - fn index(&self, index: usize) -> &Self::Output { - if index == 0 { - &self.constant - } else if index <= self.public.len() { - &self.public[index - 1] - } else { - &self.private[index - 1 - self.public.len()] - } - } + #[error("Synthesis error: {0}")] + SynthesisError(#[from] SynthesisError), } /// [`Arith`] is a trait about constraint systems (R1CS, CCS, etc.), where we @@ -131,7 +71,7 @@ pub trait Arith: Clone { /// This is also the case of CCS, where `W` and `U` may be vectors of field /// elements, [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::lcccs::LCCCS`], /// or [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::cccs::CCCS`]. -pub trait ArithRelation: Arith { +pub trait ArithRelation: Arith { type Evaluation; /// Evaluates the constraint system `self` at witness `w` and instance `u`. @@ -150,7 +90,7 @@ pub trait ArithRelation: Arith { /// /// However, we use `Self::Evaluation` to represent the evaluation result /// for future extensibility. - fn eval_relation(&self, w: &W, u: &U) -> Result; + fn eval_relation(&self, w: W::Ref<'_>, u: U::Ref<'_>) -> Result; /// Checks if the evaluation result is valid. The witness `w` and instance /// `u` are also parameters, because the validity check may need information @@ -166,26 +106,10 @@ pub trait ArithRelation: Arith { /// - The evaluation `v` of relaxed R1CS in ProtoGalaxy at satisfying `W` /// and `U` should satisfy `e = Σ pow_i(β) v_i`, where `e` is the error /// term in the committed instance. - fn check_evaluation(w: &W, u: &U, v: Self::Evaluation) -> Result<(), Error>; + fn check_evaluation(w: W::Ref<'_>, u: U::Ref<'_>, v: Self::Evaluation) -> Result<(), Error>; } -pub trait Relation { - type Error; - - /// Returns a dummy witness and instance - fn dummy_witness_instance<'a>(&'a self) -> (W, U) - where - W: Dummy<&'a Self>, - U: Dummy<&'a Self>, - { - (W::dummy(self), U::dummy(self)) - } - - /// Checks if witness `w` and instance `u` satisfy the relation `self` - fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error>; -} - -impl> Relation for A { +impl> Relation for A { type Error = Error; /// Checks if witness `w` and instance `u` satisfy the constraint system @@ -193,51 +117,12 @@ impl> Relation for A { /// validity of the evaluation result. /// /// Used only for testing. - fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error> { + fn check_relation(&self, w: W::Ref<'_>, u: U::Ref<'_>) -> Result<(), Self::Error> { let e = self.eval_relation(w, u)?; Self::check_evaluation(w, u, e) } } -/// `ArithSerializer` is for serializing constraint systems. -/// -/// Currently we only support converting parameters to bytes, but in the future -/// we may consider implementing methods for serializing the actual data (e.g., -/// R1CS matrices). -pub trait ArithSerializer { - /// Returns the bytes that represent the parameters, that is, the matrices sizes, the amount of - /// public inputs, etc, without the matrices/polynomials values. - fn params_to_le_bytes(&self) -> Vec; -} - -/// `ArithSampler` allows sampling random pairs of witness and instance that -/// satisfy the constraint system `self`. -/// -/// This is useful for constructing a zero-knowledge layer for a folding-based -/// IVC. -/// An example of such a layer can be found in Appendix D of the [HyperNova] -/// paper. -/// -/// Note that we use a separate trait for sampling, because this operation may -/// not be supported by all witness-instance pairs. -/// For instance, it is difficult (if not impossible) to do this for `w` and `x` -/// in a plain R1CS. -/// -/// [HyperNova]: https://eprint.iacr.org/2023/573.pdf -pub trait ArithSampler { - fn sample_witness_instance() { - todo!() - } -} -// pub trait ArithSampler: ArithRelation { -// /// Samples a random witness and instance that satisfy the constraint system. -// fn sample_witness_instance>( -// &self, -// params: &CS::ProverParams, -// rng: impl RngCore, -// ) -> Result<(W, U), Error>; -// } - /// `ArithRelationGadget` defines the in-circuit counterparts of operations /// specified in `ArithRelation` on constraint systems. pub trait ArithRelationGadget { diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index 9aadc648b..f31fd632f 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -3,9 +3,9 @@ use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::{borrow::Borrow, marker::PhantomData, One}; -use super::R1CS; use crate::{ - arithmetizations::{ArithRelationGadget, AssignmentsVar}, + arithmetizations::ArithRelationGadget, + circuits::Assignments, gadgets::math::{ eq::EquivalenceGadget, matrix::{MatrixGadget, SparseMatrixVar}, @@ -13,6 +13,8 @@ use crate::{ }, }; +use super::R1CS; + /// An in-circuit representation of the `R1CS` struct. /// /// `M` is for the modulo operation involved in the satisfiability check when @@ -51,9 +53,9 @@ where SparseMatrixVar: MatrixGadget, [FVar]: VectorGadget, { - pub fn eval_at_z( + pub fn eval_assignments( &self, - z: AssignmentsVar, + z: Assignments>, ) -> Result<(Vec, Vec), SynthesisError> { // Multiply Cz by z[0] (u) here, allowing this method to be reused for // both relaxed and unrelaxed R1CS. @@ -79,7 +81,7 @@ where type Evaluation = (Vec, Vec); fn eval_relation(&self, w: &WVar, u: &UVar) -> Result { - self.eval_at_z((FVar::one(), u.as_ref(), w.as_ref()).into()) + self.eval_assignments((FVar::one(), u.as_ref(), w.as_ref()).into()) } fn enforce_evaluation( @@ -91,4 +93,4 @@ where } } -// TODO: add back tests \ No newline at end of file +// TODO: add back tests diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 4804ee19e..76eae96ce 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -1,14 +1,17 @@ use ark_ff::Field; use ark_relations::gr1cs::{ConstraintSystem, Matrix}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{cfg_into_iter, cfg_iter, rand::Rng}; +use ark_std::{cfg_into_iter, cfg_iter}; #[cfg(feature = "parallel")] use rayon::prelude::*; -use sonobe_traits::Dummy; +use crate::{ + circuits::{Assignments, ConstraintSystemExt}, + relations::{Referenceable, WitnessInstanceExtractor}, + traits::Dummy, +}; -use super::{ccs::CCS, Arith, ArithRelation, ArithSerializer}; -use crate::arithmetizations::{Assignments, Error}; +use super::{ccs::CCS, Arith, ArithRelation, Error}; pub mod circuits; @@ -23,16 +26,21 @@ pub struct R1CS { } impl R1CS { - /// Evaluates the R1CS relation at a given vector of variables `z` - pub fn eval_at_z(&self, z: Assignments) -> Result, Error> { - if z.public.len() != self.n_public_inputs() { + /// Evaluates the R1CS relation at a given vector of assignments `z` + pub fn eval_assignments( + &self, + z: Assignments + Sync>, + ) -> Result, Error> { + let public_len = z.public.as_ref().len(); + let private_len = z.private.as_ref().len(); + if public_len != self.n_public_inputs() { return Err(Error::MalformedAssignments( - format!("The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", self.n_public_inputs(), z.public.len()) + format!("The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", self.n_public_inputs(), public_len) )); } - if z.private.len() != self.n_witnesses() { + if private_len != self.n_witnesses() { return Err(Error::MalformedAssignments( - format!("The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", self.n_witnesses(), z.private.len()) + format!("The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", self.n_witnesses(), private_len) )); } @@ -76,34 +84,6 @@ impl Arith for R1CS { } } -impl, U: AsRef<[F]>> ArithRelation for R1CS { - type Evaluation = Vec; - - fn eval_relation(&self, w: &W, u: &U) -> Result { - self.eval_at_z((F::one(), u.as_ref(), w.as_ref()).into()) - } - - fn check_evaluation(_w: &W, _u: &U, e: Self::Evaluation) -> Result<(), Error> { - cfg_into_iter!(e) - .all(|i| i.is_zero()) - .then_some(()) - .ok_or(Error::UnsatisfiedAssignments( - "Evaluation contains non-zero values".into(), - )) - } -} - -impl ArithSerializer for R1CS { - fn params_to_le_bytes(&self) -> Vec { - [ - self.l.to_le_bytes(), - self.m.to_le_bytes(), - self.n.to_le_bytes(), - ] - .concat() - } -} - impl Dummy<(usize, usize, usize)> for R1CS { fn dummy((n_constraints, n_variables, n_public_inputs): (usize, usize, usize)) -> Self { Self { @@ -124,28 +104,20 @@ impl R1CS { pub fn new( (n_constraints, n_variables, n_public_inputs): (usize, usize, usize), - mut matrices: Vec>, - ) -> Result { - // R1CS should have exactly 3 matrices (A, B, C) - if matrices.len() != 3 { - return Err(Error::ConstraintExtractionFailure(format!( - "R1CS should only have 3 matrices (A, B, C) but found {} matrices", - matrices.len() - ))); - } - + matrices: [Matrix; 3], + ) -> Self { + let mut matrices = matrices.to_vec(); let C = matrices.pop().unwrap(); let B = matrices.pop().unwrap(); let A = matrices.pop().unwrap(); - - Ok(Self { + Self { m: n_constraints, n: n_variables, l: n_public_inputs, A, B, C, - }) + } } } @@ -153,53 +125,193 @@ impl TryFrom> for R1CS { type Error = Error; fn try_from(ccs: CCS) -> Result { - Self::new( + if ccs.t != 3 { + return Err(Error::ConstraintExtractionFailure(format!( + "R1CS should only have 3 matrices (A, B, C) but found {} matrices", + ccs.t + ))); + } + Ok(Self::new( ( ccs.n_constraints(), ccs.n_variables(), ccs.n_public_inputs(), ), - ccs.M, - ) + ccs.M.try_into().unwrap(), + )) + } +} + +impl ArithRelation, Vec> for R1CS { + type Evaluation = Vec; + + fn eval_relation(&self, w: &[F], x: &[F]) -> Result { + self.eval_assignments((F::one(), x.as_ref(), w.as_ref()).into()) + } + + fn check_evaluation(_w: &[F], _x: &[F], e: Self::Evaluation) -> Result<(), Error> { + cfg_into_iter!(e) + .all(|i| i.is_zero()) + .then_some(()) + .ok_or(Error::UnsatisfiedAssignments( + "Evaluation contains non-zero values".into(), + )) + } +} + +impl WitnessInstanceExtractor, Vec> for R1CS { + type Source = Assignments>; + type Error = Error; + + fn extract(&self, z: Self::Source) -> Result<(Vec, Vec), Error> { + Ok((z.private, z.public)) + } +} + +pub struct RelaxedWitness { + pub w: Vec, + pub e: Vec, +} + +impl Referenceable for RelaxedWitness { + type Ref<'a> = (&'a [F], &'a [F]); + + fn reference(&self) -> Self::Ref<'_> { + (&self.w, &self.e) + } +} + +pub struct RelaxedInstance { + pub x: Vec, + pub u: F, +} + +impl Referenceable for RelaxedInstance { + type Ref<'a> = (&'a [F], F); + + fn reference(&self) -> Self::Ref<'_> { + (&self.x, self.u) + } +} + +impl ArithRelation, RelaxedInstance> for R1CS { + type Evaluation = Vec; + + fn eval_relation( + &self, + (w, _e): (&[F], &[F]), + (x, u): (&[F], F), + ) -> Result { + self.eval_assignments((u, x, w).into()) + } + + fn check_evaluation( + (_w, e): (&[F], &[F]), + _: (&[F], F), + v: Self::Evaluation, + ) -> Result<(), Error> { + cfg_iter!(e) + .zip(&v) + .all(|(e, v)| e == v) + .then_some(()) + .ok_or(Error::UnsatisfiedAssignments( + "Evaluation does not match error term".into(), + )) } } -/// Extracts R1CS from arkworks ConstraintSystem matrices -impl TryFrom<&ConstraintSystem> for R1CS { +impl WitnessInstanceExtractor, RelaxedInstance> for R1CS { + type Source = Assignments>; type Error = Error; - fn try_from(cs: &ConstraintSystem) -> Result { + fn extract(&self, z: Self::Source) -> Result<(RelaxedWitness, RelaxedInstance), Error> { + let (w, x) = self.extract(z)?; + let e = vec![F::zero(); self.n_constraints()]; + Ok((RelaxedWitness { w, e }, RelaxedInstance { x, u: F::one() })) + } +} + +impl ConstraintSystemExt for ConstraintSystem { + type Arith = R1CS; + type Error = Error; + + fn constraints(&self) -> Result, Error> { // Get the R1CS predicate matrices - let r1cs_predicate = cs.predicate_constraint_systems.get("R1CS").ok_or_else(|| { - Error::ConstraintExtractionFailure( - "No R1CS predicate found in constraint system".into(), - ) - })?; - Self::new( + let r1cs_predicate = self + .predicate_constraint_systems + .get("R1CS") + .ok_or_else(|| { + Error::ConstraintExtractionFailure( + "No R1CS predicate found in constraint system".into(), + ) + })?; + let matrices = r1cs_predicate.to_matrices(self); + if matrices.len() != 3 { + return Err(Error::ConstraintExtractionFailure(format!( + "R1CS should only have 3 matrices (A, B, C) but found {} matrices", + matrices.len() + ))); + } + Ok(R1CS::new( ( - cs.num_constraints(), - cs.num_instance_variables + cs.num_witness_variables, - cs.num_instance_variables - 1, // -1 to subtract the first '1' + self.num_constraints(), + self.num_instance_variables + self.num_witness_variables, + self.num_instance_variables - 1, // -1 to subtract the first '1' ), - r1cs_predicate.to_matrices(cs), - ) - } -} - -/// extracts the witness and the public inputs from arkworks ConstraintSystem. -pub fn extract_w_x(cs: &ConstraintSystem) -> (Vec, Vec) { - let witness = cs - .witness_assignment() - .expect("witness_assignment failed") - .to_vec(); - let instance = cs - .instance_assignment() - .expect("instance_assignment failed"); - ( - witness, + matrices.try_into().unwrap(), + )) + } + + fn assignments(&self) -> Result>, Error> { + let witness = self.witness_assignment()?.to_vec(); // skip the first element which is '1' - instance[1..].to_vec(), - ) + let instance = self.instance_assignment()?[1..].to_vec(); + + Ok((F::one(), instance, witness).into()) + } } -// TODO: add back tests \ No newline at end of file +#[cfg(test)] +pub mod tests { + use ark_bn254::Fr; + use ark_ff::UniformRand; + use ark_relations::gr1cs::ConstraintSynthesizer; + use ark_std::{error::Error, test_rng}; + + use crate::circuits::utils::{ + constraints_for_test, satisfying_assignments_for_test, CircuitForTest, + }; + + use super::*; + + #[test] + fn test_constraint_extraction() -> Result<(), Box> { + let mut rng = test_rng(); + let circuit = CircuitForTest:: { + x: Fr::rand(&mut rng), + }; + let cs = ConstraintSystem::new_ref(); + circuit.generate_constraints(cs.clone()).unwrap(); + assert!(cs.is_satisfied()?); + cs.finalize(); + let cs = cs.into_inner().unwrap(); + + assert_eq!(cs.constraints()?, constraints_for_test()); + Ok(()) + } + + #[test] + fn test_witness_extraction() -> Result<(), Box> { + let mut rng = test_rng(); + let x = Fr::rand(&mut rng); + let circuit = CircuitForTest:: { x }; + let cs = ConstraintSystem::new_ref(); + circuit.generate_constraints(cs.clone()).unwrap(); + assert!(cs.is_satisfied()?); + cs.finalize(); + let cs = cs.into_inner().unwrap(); + + assert_eq!(cs.assignments()?, satisfying_assignments_for_test(x)); + Ok(()) + } +} diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs new file mode 100644 index 000000000..0a0069ea1 --- /dev/null +++ b/crates/primitives/src/circuits/mod.rs @@ -0,0 +1,161 @@ +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::gr1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, +}; +use ark_std::{marker::PhantomData, ops::{Index, IndexMut}}; + +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). +/// The parameter z_i denotes the current state, and z_{i+1} denotes the next state after applying +/// the step. +/// Note that the external inputs for the specific circuit are defined at the implementation of +/// both `FCircuit::ExternalInputs` and `FCircuit::ExternalInputsVar`, where the `Default` trait +/// implementation for the `ExternalInputs` returns the initialized data structure (ie. if the type +/// contains a vector, it is initialized at the expected length). +pub trait FCircuit { + type ExternalInputs; + + /// returns the number of elements in the state of the FCircuit, which corresponds to the + /// FCircuit inputs. + fn state_len(&self) -> usize; + + /// generates the constraints for the step of F for the given z_i + fn generate_step_constraints( + // this method uses self, so that each FCircuit implementation (and different frontends) + // can hold a state if needed to store data to generate the constraints. + &self, + cs: ConstraintSystemRef, + i: usize, + z_i: Vec>, + external_inputs: Self::ExternalInputs, // inputs that are not part of the state + ) -> Result>, SynthesisError>; +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Assignments { + pub constant: F, + pub public: V, + pub private: V, +} + +pub type AssignmentsOwned = Assignments>; +pub type AssignmentsRef<'a, F> = Assignments; + +impl From<(F, V, V)> for Assignments { + fn from((u, x, w): (F, V, V)) -> Self { + Self { + constant: u, + public: x, + private: w, + } + } +} + +impl> Index for Assignments { + type Output = F; + + fn index(&self, index: usize) -> &Self::Output { + let public = self.public.as_ref(); + let private = self.private.as_ref(); + if index == 0 { + &self.constant + } else if index <= public.len() { + &public[index - 1] + } else { + &private[index - 1 - public.len()] + } + } +} + +impl + AsMut<[F]>> IndexMut for Assignments { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + let public = self.public.as_mut(); + let private = self.private.as_mut(); + if index == 0 { + &mut self.constant + } else if index <= public.len() { + &mut public[index - 1] + } else { + &mut private[index - 1 - public.len()] + } + } +} + +pub struct ConstraintSystemBuilder { + _f: PhantomData, + mode: SynthesisMode, + circuit: C, +} + +impl ConstraintSystemBuilder<(), ()> { + pub fn new() -> Self { + Self { + _f: PhantomData, + mode: SynthesisMode::Prove { + construct_matrices: true, + generate_lc_assignments: true, + }, + circuit: (), + } + } +} + +impl Default for ConstraintSystemBuilder<(), ()> { + fn default() -> Self { + Self::new() + } +} + +impl ConstraintSystemBuilder { + pub fn with_setup_mode(self) -> Self { + Self { + _f: PhantomData, + mode: SynthesisMode::Setup, + circuit: self.circuit, + } + } + + pub fn with_prove_mode(self) -> Self { + Self { + _f: PhantomData, + mode: SynthesisMode::Prove { + construct_matrices: true, + generate_lc_assignments: true, + }, + circuit: self.circuit, + } + } + + pub fn with_circuit>( + self, + circuit: C, + ) -> ConstraintSystemBuilder { + ConstraintSystemBuilder { + _f: PhantomData, + mode: self.mode, + circuit, + } + } +} + +impl> ConstraintSystemBuilder { + pub fn synthesize(self) -> Result, SynthesisError> { + let cs = ConstraintSystem::::new_ref(); + cs.set_mode(self.mode); + self.circuit.generate_constraints(cs.clone())?; + cs.finalize(); + Ok(cs.into_inner().unwrap()) + } +} + +pub trait ConstraintSystemExt: Sized { + type Error; + type Arith; + + fn constraints(&self) -> Result; + + fn assignments(&self) -> Result>, Self::Error>; +} diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs new file mode 100644 index 000000000..2b01cf1ba --- /dev/null +++ b/crates/primitives/src/circuits/utils.rs @@ -0,0 +1,80 @@ +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::AllocatedFp}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError, Variable}; + +use crate::arithmetizations::r1cs::R1CS; + +use super::Assignments; + +pub struct CircuitForTest { + pub x: F, +} +impl ConstraintSynthesizer for CircuitForTest { + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + // Variable 0 (implicitly added by arkworks as 1) + // Variable 1 + let x = AllocatedFp::new_input(cs.clone(), || Ok(self.x))?; + // Variable 2 + let y = AllocatedFp::new_witness(cs.clone(), || Ok(self.x.pow([3]) + self.x + F::from(5)))?; + + // Variable 3, Constraint 0 + let x_square = x.square()?; + // Variable 4, Constraint 1 + let x_cube = x_square.mul(&x); + // Variable 5 + let t = AllocatedFp::new_witness(cs.clone(), || Ok(self.x.pow([3]) + self.x))?; + let x_cube_plus_x = x.add(&x_cube); + // Constraint 2 + cs.enforce_r1cs_constraint( + || x_cube_plus_x.variable.into(), + || Variable::one().into(), + || t.variable.into(), + )?; + let x_cube_plus_x_plus_5 = t.add_constant(F::from(5)); + // Constraint 3 + cs.enforce_r1cs_constraint( + || x_cube_plus_x_plus_5.variable.into(), + || Variable::one().into(), + || y.variable.into(), + )?; + Ok(()) + } +} + +pub fn constraints_for_test() -> R1CS { + // R1CS for: x^3 + x + 5 = y (example from article + // https://vitalik.eth.limo/general/2016/12/10/qap.html) + let A = vec![ + vec![(F::one(), 1)], + vec![(F::one(), 3)], + vec![(F::one(), 1), (F::one(), 4)], + vec![(F::from(5), 0), (F::one(), 5)], + ]; + let B = vec![ + vec![(F::one(), 1)], + vec![(F::one(), 1)], + vec![(F::one(), 0)], + vec![(F::one(), 0)], + ]; + let C = vec![ + vec![(F::one(), 3)], + vec![(F::one(), 4)], + vec![(F::one(), 5)], + vec![(F::one(), 2)], + ]; + + R1CS::::new((4, 6, 1), [A, B, C]) +} + +pub fn satisfying_assignments_for_test(x: F) -> Assignments> { + Assignments::from(( + F::one(), + vec![x], + vec![ + x * x * x + x + F::from(5), // x^3 + x + 5 + x * x, // x^2 + x * x * x, // x^2 * x + x * x * x + x, // x^3 + x + ], + )) +} diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index dac037619..593ba7879 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,11 +1,11 @@ -use ark_r1cs_std::alloc::AllocVar; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::fmt::Debug; -use ark_std::rand::RngCore; +use ark_std::{ + fmt::Debug, + iter::Sum, + ops::{Add, Mul}, + rand::RngCore, +}; use thiserror::Error; -use sonobe_traits::Curve; - pub mod pedersen; // TODO: add back other commitment schemes @@ -22,20 +22,31 @@ pub enum Error { CommitmentVerificationFail, } -pub trait VectorCommitment { +pub trait VectorCommitment: 'static + Debug + PartialEq { const IS_HIDING: bool; type Key; - type Scalar; - type Commitment; - type Randomness; + type Scalar: Clone + Copy + Debug + PartialEq + Sync; + type Commitment: Default + Debug + PartialEq + Sync; + type Randomness: Clone + + Copy + + Default + + Debug + + Sync + + Add + + Mul + + for<'a> Add<&'a Self::Scalar, Output = Self::Randomness> + + for<'a> Mul<&'a Self::Scalar, Output = Self::Randomness> + + Add + + Mul + + Sum; - fn generate_key(rng: &mut impl RngCore, len: usize) -> Result; + fn generate_key(rng: impl RngCore, len: usize) -> Result; fn commit( ck: &Self::Key, v: &[Self::Scalar], - rng: &mut impl RngCore, + rng: impl RngCore, ) -> Result<(Self::Commitment, Self::Randomness), Error>; fn open( @@ -53,14 +64,16 @@ mod tests { use super::*; - pub fn test_commitment_opt>( - rng: &mut impl RngCore, + pub fn test_commitment_correctness>( + mut rng: impl RngCore, len: usize, ) -> Result<(), Box> { - let v = (0..len).map(|_| VC::Scalar::rand(rng)).collect::>(); + let v = (0..len) + .map(|_| VC::Scalar::rand(&mut rng)) + .collect::>(); - let ck = VC::generate_key(rng, len)?; - let (cm, r) = VC::commit(&ck, &v, rng)?; + let ck = VC::generate_key(&mut rng, len)?; + let (cm, r) = VC::commit(&ck, &v, &mut rng)?; assert!(VC::open(&ck, &v, &r, &cm)?); Ok(()) } diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 0f97cca9c..3d53d7a61 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -3,14 +3,14 @@ use ark_relations::gr1cs::SynthesisError; use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; use super::{Error, VectorCommitment}; -use sonobe_traits::{Curve, CF2}; +use crate::traits::{Null, SonobeCurve, CF2}; -#[derive(Debug)] -pub struct Pedersen { +#[derive(Debug, PartialEq)] +pub struct Pedersen { _c: PhantomData, } -impl Pedersen { +impl Pedersen { fn msm(g: &[C::Affine], v: &[C::ScalarField]) -> Result { if g.len() < v.len() { return Err(Error::MessageTooLong(g.len(), v.len())); @@ -21,16 +21,16 @@ impl Pedersen { } } -impl VectorCommitment for Pedersen { +impl VectorCommitment for Pedersen { const IS_HIDING: bool = false; type Key = Vec; type Scalar = C::ScalarField; type Commitment = C; - type Randomness = (); + type Randomness = Null; - fn generate_key(rng: &mut impl RngCore, len: usize) -> Result { - let generators = repeat_with(|| C::rand(rng)) + fn generate_key(mut rng: impl RngCore, len: usize) -> Result { + let generators = repeat_with(|| C::rand(&mut rng)) .take(len.next_power_of_two()) .collect::>(); Ok(C::normalize_batch(&generators)) @@ -39,9 +39,9 @@ impl VectorCommitment for Pedersen { fn commit( g: &Self::Key, v: &[Self::Scalar], - _rng: &mut impl RngCore, + _rng: impl RngCore, ) -> Result<(Self::Commitment, Self::Randomness), Error> { - Ok((Self::msm(g, v)?, ())) + Ok((Self::msm(g, v)?, Null)) } fn open( @@ -54,7 +54,7 @@ impl VectorCommitment for Pedersen { } } -impl VectorCommitment for Pedersen { +impl VectorCommitment for Pedersen { const IS_HIDING: bool = true; type Key = (Vec, C); @@ -62,16 +62,19 @@ impl VectorCommitment for Pedersen { type Commitment = C; type Randomness = C::ScalarField; - fn generate_key(rng: &mut impl RngCore, len: usize) -> Result { - Ok((Pedersen::::generate_key(rng, len)?, C::rand(rng))) + fn generate_key(mut rng: impl RngCore, len: usize) -> Result { + Ok(( + Pedersen::::generate_key(&mut rng, len)?, + C::rand(&mut rng), + )) } fn commit( (g, h): &Self::Key, v: &[Self::Scalar], - rng: &mut impl RngCore, + mut rng: impl RngCore, ) -> Result<(Self::Commitment, Self::Randomness), Error> { - let r = C::ScalarField::rand(rng); + let r = C::ScalarField::rand(&mut rng); Ok((Self::msm(g, v)? + h.mul(r), r)) } @@ -85,11 +88,11 @@ impl VectorCommitment for Pedersen { } } -pub struct PedersenGadget { +pub struct PedersenGadget { _c: PhantomData, } -impl PedersenGadget { +impl PedersenGadget { pub fn commit( h: &C::Var, g: &[C::Var], @@ -123,25 +126,20 @@ impl PedersenGadget { #[cfg(test)] mod tests { - use ark_bn254::{constraints::GVar, Fq, Fr, G1Projective}; - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; - use ark_ff::{BigInteger, PrimeField}; - use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget}; - use ark_relations::gr1cs::ConstraintSystem; + use ark_bn254::G1Projective; use ark_std::{error::Error, rand::Rng, test_rng}; - use crate::commitments::tests::test_commitment_opt; + use crate::commitments::tests::test_commitment_correctness; use super::*; - use crate::transcripts::poseidon::poseidon_canonical_config; #[test] fn test_pedersen_commitment() -> Result<(), Box> { - let rng = &mut test_rng(); + let mut rng = test_rng(); for i in 0..10 { let len = rng.gen_range((1 << i)..(1 << (i + 1))); - test_commitment_opt::>(rng, len)?; - test_commitment_opt::>(rng, len)?; + test_commitment_correctness::>(&mut rng, len)?; + test_commitment_correctness::>(&mut rng, len)?; } Ok(()) } diff --git a/crates/primitives/src/gadgets/nonnative/affine.rs b/crates/primitives/src/gadgets/nonnative/affine.rs index 182525cc9..c290925d6 100644 --- a/crates/primitives/src/gadgets/nonnative/affine.rs +++ b/crates/primitives/src/gadgets/nonnative/affine.rs @@ -11,7 +11,7 @@ use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; use ark_std::{borrow::Borrow, Zero}; -use sonobe_traits::{AbsorbNonNativeGadget, Curve}; +use crate::traits::{AbsorbNonNativeGadget, SonobeCurve}; use super::uint::NonNativeUintVar; @@ -19,12 +19,12 @@ use super::uint::NonNativeUintVar; /// field, over the constraint field. It is not intended to perform operations, but just to contain /// the affine coordinates in order to perform hash operations of the point. #[derive(Debug, Clone)] -pub struct NonNativeAffineVar { +pub struct NonNativeAffineVar { pub x: NonNativeUintVar, pub y: NonNativeUintVar, } -impl AllocVar for NonNativeAffineVar { +impl AllocVar for NonNativeAffineVar { fn new_variable>( cs: impl Into>, f: impl FnOnce() -> Result, @@ -44,7 +44,7 @@ impl AllocVar for NonNativeAffineVar { } } -impl GR1CSVar for NonNativeAffineVar { +impl GR1CSVar for NonNativeAffineVar { type Value = C; fn cs(&self) -> ConstraintSystemRef { @@ -81,7 +81,7 @@ impl GR1CSVar for NonNativeAffineVar { } } -impl EqGadget for NonNativeAffineVar { +impl EqGadget for NonNativeAffineVar { fn is_eq(&self, other: &Self) -> Result, SynthesisError> { let mut result = Boolean::TRUE; if self.x.0.len() != other.x.0.len() { @@ -128,7 +128,7 @@ impl EqGadget for NonNativeAffineVar { } } -impl NonNativeAffineVar { +impl NonNativeAffineVar { pub fn zero() -> Self { // `unwrap` below is safe because we are allocating a constant value, // which is guaranteed to succeed. @@ -136,7 +136,7 @@ impl NonNativeAffineVar { } } -impl AbsorbNonNativeGadget for NonNativeAffineVar { +impl AbsorbNonNativeGadget for NonNativeAffineVar { fn to_native_sponge_field_elements( &self, ) -> Result>, SynthesisError> { @@ -150,7 +150,7 @@ mod tests { use ark_r1cs_std::groups::curves::short_weierstrass::ProjectiveVar; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, UniformRand}; - use sonobe_traits::{AbsorbNonNative, Inputize, InputizeNonNative}; + use crate::traits::{AbsorbNonNative, Inputize, InputizeNonNative}; use super::*; diff --git a/crates/primitives/src/gadgets/nonnative/uint.rs b/crates/primitives/src/gadgets/nonnative/uint.rs index 754908392..fa0db0e69 100644 --- a/crates/primitives/src/gadgets/nonnative/uint.rs +++ b/crates/primitives/src/gadgets/nonnative/uint.rs @@ -1,5 +1,3 @@ -use std::ops::Index; - use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, @@ -14,16 +12,18 @@ use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{ borrow::Borrow, cmp::{max, min}, + ops::Index, }; use num_bigint::BigUint; use num_integer::Integer; -use sonobe_traits::{AbsorbNonNativeGadget, Field}; - -use crate::gadgets::math::{ - eq::EquivalenceGadget, - matrix::{MatrixGadget, SparseMatrixVar}, - vector::VectorGadget, +use crate::{ + gadgets::math::{ + eq::EquivalenceGadget, + matrix::{MatrixGadget, SparseMatrixVar}, + vector::VectorGadget, + }, + traits::{AbsorbNonNativeGadget, SonobeField}, }; /// `LimbVar` represents a single limb of a non-native unsigned integer in the @@ -242,7 +242,7 @@ impl AllocVar for NonNativeUintVar { } } -impl AllocVar for NonNativeUintVar { +impl AllocVar for NonNativeUintVar { fn new_variable>( cs: impl Into>, f: impl FnOnce() -> Result, @@ -799,8 +799,7 @@ impl AbsorbNonNativeGadget for NonNativeUintVar { fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; - self - .to_bits_le()? + self.to_bits_le()? .chunks(bits_per_limb) .map(Boolean::le_bits_to_fp) .collect() diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 9400c51d2..b6ced84d8 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -1,4 +1,8 @@ -pub mod commitments; pub mod arithmetizations; +pub mod circuits; +pub mod commitments; pub mod gadgets; +pub mod relations; +pub mod sumcheck; +pub mod traits; pub mod transcripts; diff --git a/crates/primitives/src/relations/mod.rs b/crates/primitives/src/relations/mod.rs new file mode 100644 index 000000000..c9349c2dd --- /dev/null +++ b/crates/primitives/src/relations/mod.rs @@ -0,0 +1,52 @@ +use ark_std::{error::Error, rand::RngCore}; + +use crate::traits::Dummy; + +pub trait Referenceable { + type Ref<'a>: Copy + where + Self: 'a; + + fn reference(&self) -> Self::Ref<'_>; +} + +impl Referenceable for Vec { + type Ref<'a> = &'a [T]; + + fn reference(&self) -> Self::Ref<'_> { + self + } +} + +pub trait Relation { + type Error: Error; + + /// Checks if witness `w` and instance `u` satisfy the relation `self` + fn check_relation(&self, w: W::Ref<'_>, u: U::Ref<'_>) -> Result<(), Self::Error>; +} + +pub trait WitnessInstanceExtractor { + type Source; + type Error: Error + 'static; + + fn extract(&self, source: Self::Source) -> Result<(W, U), Self::Error>; +} + +pub trait WitnessInstanceInitializer { + fn dummy_witness_instance<'a>(&'a self) -> (W, U) + where + W: Dummy<&'a Self>, + U: Dummy<&'a Self>, + { + (W::dummy(self), U::dummy(self)) + } +} + +/// `WitnessInstanceSampler` allows sampling a random witness-instance pair that +/// satisfies the relation `self`. +pub trait WitnessInstanceSampler { + type Source; + type Error: Error + 'static; + + fn sample(&self, source: Self::Source, rng: impl RngCore) -> Result<(W, U), Self::Error>; +} diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs new file mode 100644 index 000000000..2868f5828 --- /dev/null +++ b/crates/primitives/src/sumcheck/mod.rs @@ -0,0 +1,291 @@ +// code forked from: +// https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check +// +// Copyright (c) 2023 Espresso Systems (espressosys.com) +// This file is part of the HyperPlonk library. + +// You should have received a copy of the MIT License +// along with the HyperPlonk library. If not, see . + +//! This module implements the sum check protocol. + +use ark_crypto_primitives::sponge::Absorb; +use ark_ff::PrimeField; +use ark_poly::{ + univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, Polynomial, +}; +use ark_std::{cfg_chunks, cfg_into_iter, cfg_iter, fmt::Debug}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use thiserror::Error; + +use crate::transcripts::Transcript; + +use utils::{ + barycentric_weights, compute_lagrange_interpolated_poly, extrapolate, VPAuxInfo, + VirtualPolynomial, +}; + +pub mod utils; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Invalid Polynomial IOP Prover: {0}")] + InvalidPolyIOPProver(String), + #[error("Invalid Polynomial IOP Verifier: {0}")] + InvalidPolyIOPVerifier(String), + #[error("Invalid Polynomial IOP Proof: {0}")] + InvalidPolyIOPProof(String), + #[error("Invalid Polynomial IOP Parameters: {0}")] + InvalidPolyIOPParameters(String), +} + +/// An IOP proof is a collections of +/// - messages from prover to verifier at each round through the interactive +/// protocol. +/// - a point that is generated by the transcript for evaluation +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct IOPProof { + pub point: Vec, + pub proofs: Vec>, +} + +/// A SumCheckSubClaim is a claim generated by the verifier at the end of +/// verification when it is convinced. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SumCheckSubClaim { + /// the multi-dimensional point that this multilinear extension is evaluated + /// to + pub point: Vec, + /// the expected evaluation + pub expected_evaluation: F, +} + +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] +pub struct IOPSumCheck; + +impl IOPSumCheck { + pub fn prove( + mut poly: VirtualPolynomial, + transcript: &mut impl Transcript, + ) -> Result<(IOPProof, Vec>), Error> { + transcript.absorb(&F::from(poly.aux_info.num_variables as u64)); + transcript.absorb(&F::from(poly.aux_info.max_degree as u64)); + let extrapolation_aux = (1..poly.aux_info.max_degree) + .map(|degree| { + let points = (0..1 + degree as u64).map(F::from).collect::>(); + let weights = barycentric_weights(&points); + (points, weights) + }) + .collect::>(); + let mut prover_msgs = Vec::with_capacity(poly.aux_info.num_variables); + let mut challenges = Vec::with_capacity(poly.aux_info.num_variables); + for i in 0..poly.aux_info.num_variables { + let mut products_sum = vec![F::ZERO; poly.aux_info.max_degree + 1]; + + // Step 2: generate sum for the partial evaluated polynomial: + // f(r_1, ... r_m,, x_{m+1}... x_n) + + poly.products.iter().for_each(|(coefficient, products)| { + #[cfg(feature = "parallel")] + let mut sum = cfg_into_iter!(0..1 << (poly.aux_info.num_variables - i - 1)) + .fold( + || { + ( + vec![(F::ZERO, F::ZERO); products.len()], + vec![F::ZERO; products.len() + 1], + ) + }, + |(mut buf, mut acc), b| { + buf.iter_mut() + .zip(products.iter()) + .for_each(|((eval, step), f)| { + let table = &poly.flattened_ml_extensions[*f]; + *eval = table[b << 1]; + *step = table[(b << 1) + 1] - table[b << 1]; + }); + acc[0] += buf.iter().map(|(eval, _)| eval).product::(); + acc[1..].iter_mut().for_each(|acc| { + buf.iter_mut().for_each(|(eval, step)| *eval += step); + *acc += buf.iter().map(|(eval, _)| eval).product::(); + }); + (buf, acc) + }, + ) + .map(|(_, partial)| partial) + .reduce( + || vec![F::ZERO; products.len() + 1], + |mut sum, partial| { + sum.iter_mut() + .zip(partial.iter()) + .for_each(|(sum, partial)| *sum += partial); + sum + }, + ); + #[cfg(not(feature = "parallel"))] + let mut sum = cfg_into_iter!(0..1 << (poly.aux_info.num_variables - i - 1)) + .fold( + ( + vec![(F::ZERO, F::ZERO); products.len()], + vec![F::ZERO; products.len() + 1], + ), + |(mut buf, mut acc), b| { + buf.iter_mut() + .zip(products.iter()) + .for_each(|((eval, step), f)| { + let table = &poly.flattened_ml_extensions[*f]; + *eval = table[b << 1]; + *step = table[(b << 1) + 1] - table[b << 1]; + }); + acc[0] += buf.iter().map(|(eval, _)| eval).product::(); + acc[1..].iter_mut().for_each(|acc| { + buf.iter_mut().for_each(|(eval, step)| *eval += step as &_); + *acc += buf.iter().map(|(eval, _)| eval).product::(); + }); + (buf, acc) + }, + ) + .1; + sum.iter_mut().for_each(|sum| *sum *= coefficient); + let extraploation = cfg_into_iter!(0..poly.aux_info.max_degree - products.len()) + .map(|i| { + let (points, weights) = &extrapolation_aux[products.len() - 1]; + let at = F::from((products.len() + 1 + i) as u64); + extrapolate(points, weights, &sum, &at) + }) + .collect::>(); + products_sum + .iter_mut() + .zip(sum.iter().chain(extraploation.iter())) + .for_each(|(products_sum, sum)| *products_sum += sum); + }); + + let prover_poly = compute_lagrange_interpolated_poly(&products_sum).coeffs; + transcript.absorb(&prover_poly); + prover_msgs.push(prover_poly); + + let challenge = transcript.get_challenge(); + challenges.push(challenge); + poly.flattened_ml_extensions.iter_mut().for_each(|mle| { + mle.evaluations = cfg_chunks!(mle.evaluations, 2) + .map(|chunk| chunk[0] + challenge * (chunk[1] - chunk[0])) + .collect(); + mle.num_vars -= 1; + }); + } + + Ok(( + IOPProof { + point: challenges, + proofs: prover_msgs, + }, + poly.flattened_ml_extensions, + )) + } + + pub fn verify( + claimed_sum: F, + proof: &IOPProof, + aux_info: &VPAuxInfo, + transcript: &mut impl Transcript, + ) -> Result, Error> { + transcript.absorb(&F::from(aux_info.num_variables as u64)); + transcript.absorb(&F::from(aux_info.max_degree as u64)); + assert_eq!(aux_info.num_variables, proof.proofs.len()); + + let challenges = proof + .proofs + .iter() + .map(|msg| { + transcript.absorb(&msg); + transcript.get_challenge() + }) + .collect::>(); + + // the deferred check during the interactive phase: + // 2. set `expected` to P(r)` + let mut expected_vec = cfg_iter!(proof.proofs) + .zip(&challenges) + .map(|(coeffs, challenge)| { + DensePolynomial::from_coefficients_slice(coeffs).evaluate(challenge) + }) + .collect::>(); + + // insert the asserted_sum to the first position of the expected vector + expected_vec.insert(0, claimed_sum); + + for (coeffs, &expected) in proof.proofs.iter().zip(expected_vec.iter()) { + let eval_at_one: F = coeffs.iter().sum(); + let eval_at_zero: F = if coeffs.is_empty() { + F::zero() + } else { + coeffs[0] + }; + + // the deferred check during the interactive phase: + // 1. check if the received 'P(0) + P(1) = expected`. + if eval_at_one + eval_at_zero != expected { + return Err(Error::InvalidPolyIOPProof( + "Prover message is not consistent with the claim.".to_string(), + )); + } + } + Ok(SumCheckSubClaim { + point: challenges, + // the last expected value (not checked within this function) will be included in the + // subclaim + expected_evaluation: expected_vec[expected_vec.len() - 1], + }) + } +} + +#[cfg(test)] +pub mod tests { + use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; + use ark_ff::Field; + use ark_pallas::Fr; + use ark_poly::{DenseMultilinearExtension, MultilinearExtension}; + use ark_std::{test_rng, One, Zero}; + + use crate::transcripts::poseidon::poseidon_canonical_config; + + use super::*; + + #[test] + pub fn sumcheck_poseidon() -> Result<(), Error> { + let n_vars = 10; + + let mut rng = test_rng(); + let poly_mle = DenseMultilinearExtension::rand(n_vars, &mut rng); + let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); + + sumcheck_poseidon_opt(virtual_poly)?; + + // test with zero poly + let poly_mle = DenseMultilinearExtension::from_evaluations_vec( + n_vars, + vec![Fr::zero(); 2u32.pow(n_vars as u32) as usize], + ); + let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); + sumcheck_poseidon_opt(virtual_poly)?; + Ok(()) + } + + fn sumcheck_poseidon_opt(virtual_poly: VirtualPolynomial) -> Result<(), Error> { + let aux_info = virtual_poly.aux_info.clone(); + let poseidon_config = poseidon_canonical_config::(); + + // sum-check prove + let mut transcript_p: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); + let (sum_check, _) = IOPSumCheck::prove(virtual_poly, &mut transcript_p)?; + + // sum-check verify + let poly = DensePolynomial::from_coefficients_vec(sum_check.proofs[0].clone()); + let claimed_sum = poly.evaluate(&Fr::one()) + poly.evaluate(&Fr::zero()); + let mut transcript_v: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); + let res_verify = IOPSumCheck::verify(claimed_sum, &sum_check, &aux_info, &mut transcript_v); + + assert!(res_verify.is_ok()); + Ok(()) + } +} diff --git a/crates/primitives/src/sumcheck/utils.rs b/crates/primitives/src/sumcheck/utils.rs new file mode 100644 index 000000000..d75a973b4 --- /dev/null +++ b/crates/primitives/src/sumcheck/utils.rs @@ -0,0 +1,399 @@ +// code forked from +// https://github.com/privacy-scaling-explorations/multifolding-poc/blob/main/src/espresso/virtual_polynomial.rs +// +// Copyright (c) 2023 Espresso Systems (espressosys.com) +// This file is part of the HyperPlonk library. + +// You should have received a copy of the MIT License +// along with the HyperPlonk library. If not, see . + +//! This module defines our main mathematical object `VirtualPolynomial`; and +//! various functions associated with it. + +use ark_ff::{batch_inversion, PrimeField}; +use ark_poly::{univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial}; +use ark_serialize::CanonicalSerialize; +use ark_std::cfg_into_iter; +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +/// A virtual polynomial is a sum of products of multilinear polynomials; +/// where the multilinear polynomials are stored via their multilinear +/// extensions: `(coefficient, DenseMultilinearExtension)` +/// +/// * Number of products n = `polynomial.products.len()`, +/// * Number of multiplicands of ith product m_i = +/// `polynomial.products[i].1.len()`, +/// * Coefficient of ith product c_i = `polynomial.products[i].0` +/// +/// The resulting polynomial is +/// +/// $$ \sum_{i=0}^{n} c_i \cdot \prod_{j=0}^{m_i} P_{ij} $$ +/// +/// Example: +/// f = c0 * f0 * f1 * f2 + c1 * f3 * f4 +/// where f0 ... f4 are multilinear polynomials +/// +/// - `flattened_ml_extensions` stores the multilinear extension representation +/// of f0, f1, f2, f3 and f4 +/// - `products` is `[(c0, [0, 1, 2]), (c1, [3, 4])]` +/// - raw_pointers_lookup_table maps fi to i +/// +#[derive(Clone, Debug, Default, PartialEq)] +pub struct VirtualPolynomial { + /// Aux information about the multilinear polynomial + pub aux_info: VPAuxInfo, + pub flattened_ml_extensions: Vec>, + /// list of reference to products (as usize) of multilinear extension + pub products: Vec<(F, Vec)>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, CanonicalSerialize)] +/// Auxiliary information about the multilinear polynomial +pub struct VPAuxInfo { + /// max number of multiplicands in each product + pub max_degree: usize, + /// number of variables of the polynomial + pub num_variables: usize, +} + +// TODO: convert this into a trait +impl VirtualPolynomial { + /// Creates a new virtual polynomial from a MLE and its coefficient. + pub fn new_from_mle(mle: DenseMultilinearExtension, coefficient: F) -> Self { + VirtualPolynomial { + aux_info: VPAuxInfo { + // The max degree is the max degree of any individual variable + max_degree: 1, + num_variables: mle.num_vars, + }, + // here `0` points to the first polynomial of `flattened_ml_extensions` + products: vec![(coefficient, vec![0])], + flattened_ml_extensions: vec![mle], + } + } +} + +/// Evaluate eq polynomial. +pub fn eq_eval(x: &[F], y: &[F]) -> F { + debug_assert_eq!(x.len(), y.len()); + x.iter() + .zip(y.iter()) + .map(|(xi, yi)| xi.double() * yi - xi - yi + F::one()) + .product::() +} + +/// This function build the eq(x, r) polynomial for any given r, and output the +/// evaluation of eq(x, r) in its vector form. +/// +/// Evaluate +/// eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) +/// over r, which is +/// eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) +pub fn build_eq_x_r_vec(r: &[F]) -> Vec { + // we build eq(x,r) from its evaluations + // we want to evaluate eq(x,r) over x \in {0, 1}^num_vars + // for example, with num_vars = 4, x is a binary vector of 4, then + // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) + // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) + // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) + // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) + // .... + // 1 1 1 1 -> r0 * r1 * r2 * r3 + // we will need 2^num_var evaluations + + // initializing the buffer with [1] + let mut buf = vec![F::one()]; + + for i in r.iter().rev() { + // suppose at the previous step we received [b_1, ..., b_k] + // for the current step we will need + // if x_i = 0: (1-ri) * [b_1, ..., b_k] + // if x_i = 1: ri * [b_1, ..., b_k] + buf = cfg_into_iter!(buf) + .flat_map(|j| { + let v = j * i; + [j - v, v] + }) + .collect(); + } + + buf +} + +#[allow(clippy::filter_map_bool_then)] +pub fn barycentric_weights(points: &[F]) -> Vec { + let mut weights = points + .iter() + .enumerate() + .map(|(j, point_j)| { + points + .iter() + .enumerate() + .filter_map(|(i, point_i)| (i != j).then(|| *point_j - point_i)) + .reduce(|acc, value| acc * value) + .unwrap_or_else(F::one) + }) + .collect::>(); + batch_inversion(&mut weights); + weights +} + +pub fn extrapolate(points: &[F], weights: &[F], evals: &[F], at: &F) -> F { + let (coeffs, sum_inv) = { + let mut coeffs = points.iter().map(|point| *at - point).collect::>(); + batch_inversion(&mut coeffs); + coeffs.iter_mut().zip(weights).for_each(|(coeff, weight)| { + *coeff *= weight; + }); + let sum_inv = coeffs.iter().sum::().inverse().unwrap_or_default(); + (coeffs, sum_inv) + }; + coeffs + .iter() + .zip(evals) + .map(|(coeff, eval)| *coeff * eval) + .sum::() + * sum_inv +} + +/// Computes the lagrange interpolated polynomial from the given points `p_i` +pub fn compute_lagrange_interpolated_poly(p_i: &[F]) -> DensePolynomial { + let v = (0..p_i.len()) + .map(|i| F::from(i as u64)) + .collect::>(); + + // compute l(x), common to every basis polynomial + let mut l_x = DensePolynomial::from_coefficients_vec(vec![F::ONE]); + for i in &v { + let prod_m = DensePolynomial::from_coefficients_vec(vec![-*i, F::ONE]); + l_x = &l_x * &prod_m; + } + + // compute each w_j - barycentric weights + let w_j_vector = barycentric_weights(&v); + + // compute each polynomial within the sum L(x) + let mut lagrange_poly = DensePolynomial::from_coefficients_vec(vec![F::ZERO]); + for (j, w_j) in w_j_vector.iter().enumerate() { + let x_j = j; + let y_j = p_i[j]; + // we multiply by l(x) here, otherwise the below division will not work - deg(0)/deg(d) + let poly_numerator = &(&l_x * (*w_j)) * (y_j); + let poly_denominator = DensePolynomial::from_coefficients_vec(vec![-v[x_j], F::ONE]); + let poly = &poly_numerator / &poly_denominator; + lagrange_poly = &lagrange_poly + &poly; + } + + lagrange_poly +} + +#[cfg(test)] +mod tests { + use ark_pallas::Fr; + use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; + use ark_std::UniformRand; + + use super::*; + + /// Interpolate a uni-variate degree-`p_i.len()-1` polynomial and evaluate this + /// polynomial at `eval_at`: + /// + /// \sum_{i=0}^len p_i * (\prod_{j!=i} (eval_at - j)/(i-j) ) + /// + /// This implementation is linear in number of inputs in terms of field + /// operations. It also has a quadratic term in primitive operations which is + /// negligible compared to field operations. + /// TODO: The quadratic term can be removed by precomputing the lagrange + /// coefficients. + fn interpolate_uni_poly(p_i: &[F], eval_at: F) -> F { + let len = p_i.len(); + let mut evals = vec![]; + let mut prod = eval_at; + evals.push(eval_at); + + // `prod = \prod_{j} (eval_at - j)` + for e in 1..len { + let tmp = eval_at - F::from(e as u64); + evals.push(tmp); + prod *= tmp; + } + let mut res = F::zero(); + // we want to compute \prod (j!=i) (i-j) for a given i + // + // we start from the last step, which is + // denom[len-1] = (len-1) * (len-2) *... * 2 * 1 + // the step before that is + // denom[len-2] = (len-2) * (len-3) * ... * 2 * 1 * -1 + // and the step before that is + // denom[len-3] = (len-3) * (len-4) * ... * 2 * 1 * -1 * -2 + // + // i.e., for any i, the one before this will be derived from + // denom[i-1] = denom[i] * (len-i) / i + // + // that is, we only need to store + // - the last denom for i = len-1, and + // - the ratio between current step and fhe last step, which is the product of + // (len-i) / i from all previous steps and we store this product as a fraction + // number to reduce field divisions. + + // We know + // - 2^61 < factorial(20) < 2^62 + // - 2^122 < factorial(33) < 2^123 + // so we will be able to compute the ratio + // - for len <= 20 with i64 + // - for len <= 33 with i128 + // - for len > 33 with BigInt + if p_i.len() <= 20 { + let last_denominator = F::from(u64_factorial(len - 1)); + let mut ratio_numerator = 1i64; + let mut ratio_denominator = 1u64; + + for i in (0..len).rev() { + let ratio_numerator_f = if ratio_numerator < 0 { + -F::from((-ratio_numerator) as u64) + } else { + F::from(ratio_numerator as u64) + }; + + res += p_i[i] * prod * F::from(ratio_denominator) + / (last_denominator * ratio_numerator_f * evals[i]); + + // compute denom for the next step is current_denom * (len-i)/i + if i != 0 { + ratio_numerator *= -(len as i64 - i as i64); + ratio_denominator *= i as u64; + } + } + } else if p_i.len() <= 33 { + let last_denominator = F::from(u128_factorial(len - 1)); + let mut ratio_numerator = 1i128; + let mut ratio_denominator = 1u128; + + for i in (0..len).rev() { + let ratio_numerator_f = if ratio_numerator < 0 { + -F::from((-ratio_numerator) as u128) + } else { + F::from(ratio_numerator as u128) + }; + + res += p_i[i] * prod * F::from(ratio_denominator) + / (last_denominator * ratio_numerator_f * evals[i]); + + // compute denom for the next step is current_denom * (len-i)/i + if i != 0 { + ratio_numerator *= -(len as i128 - i as i128); + ratio_denominator *= i as u128; + } + } + } else { + let mut denom_up = field_factorial::(len - 1); + let mut denom_down = F::one(); + + for i in (0..len).rev() { + res += p_i[i] * prod * denom_down / (denom_up * evals[i]); + + // compute denom for the next step is current_denom * (len-i)/i + if i != 0 { + denom_up *= -F::from((len - i) as u64); + denom_down *= F::from(i as u64); + } + } + } + res + } + + /// compute the factorial(a) = 1 * 2 * ... * a + #[inline] + fn field_factorial(a: usize) -> F { + let mut res = F::one(); + for i in 2..=a { + res *= F::from(i as u64); + } + res + } + + /// compute the factorial(a) = 1 * 2 * ... * a + #[inline] + fn u128_factorial(a: usize) -> u128 { + let mut res = 1u128; + for i in 2..=a { + res *= i as u128; + } + res + } + + /// compute the factorial(a) = 1 * 2 * ... * a + #[inline] + fn u64_factorial(a: usize) -> u64 { + let mut res = 1u64; + for i in 2..=a { + res *= i as u64; + } + res + } + + #[test] + fn test_compute_lagrange_interpolated_poly() { + let mut prng = ark_std::test_rng(); + for degree in 1..30 { + let poly = DensePolynomial::::rand(degree, &mut prng); + // range (which is exclusive) is from 0 to degree + 1, since we need degree + 1 evaluations + let evals = (0..(degree + 1)) + .map(|i| poly.evaluate(&Fr::from(i as u64))) + .collect::>(); + let lagrange_poly = compute_lagrange_interpolated_poly(&evals); + for _ in 0..10 { + let query = Fr::rand(&mut prng); + let lagrange_eval = lagrange_poly.evaluate(&query); + let eval = poly.evaluate(&query); + assert_eq!(eval, lagrange_eval); + assert_eq!(lagrange_poly.degree(), poly.degree()); + } + } + } + + #[test] + fn test_interpolation() { + let mut prng = ark_std::test_rng(); + + // test a polynomial with 20 known points, i.e., with degree 19 + let poly = DensePolynomial::::rand(20 - 1, &mut prng); + let evals = (0..20) + .map(|i| poly.evaluate(&Fr::from(i))) + .collect::>(); + let query = Fr::rand(&mut prng); + + assert_eq!(poly.evaluate(&query), interpolate_uni_poly(&evals, query)); + assert_eq!( + compute_lagrange_interpolated_poly(&evals).evaluate(&query), + interpolate_uni_poly(&evals, query) + ); + + // test a polynomial with 33 known points, i.e., with degree 32 + let poly = DensePolynomial::::rand(33 - 1, &mut prng); + let evals = (0..33) + .map(|i| poly.evaluate(&Fr::from(i))) + .collect::>(); + let query = Fr::rand(&mut prng); + + assert_eq!(poly.evaluate(&query), interpolate_uni_poly(&evals, query)); + assert_eq!( + compute_lagrange_interpolated_poly(&evals).evaluate(&query), + interpolate_uni_poly(&evals, query) + ); + + // test a polynomial with 64 known points, i.e., with degree 63 + let poly = DensePolynomial::::rand(64 - 1, &mut prng); + let evals = (0..64) + .map(|i| poly.evaluate(&Fr::from(i))) + .collect::>(); + let query = Fr::rand(&mut prng); + + assert_eq!(poly.evaluate(&query), interpolate_uni_poly(&evals, query)); + assert_eq!( + compute_lagrange_interpolated_poly(&evals).evaluate(&query), + interpolate_uni_poly(&evals, query) + ); + } +} diff --git a/crates/traits/src/lib.rs b/crates/primitives/src/traits/mod.rs similarity index 73% rename from crates/traits/src/lib.rs rename to crates/primitives/src/traits/mod.rs index 1106def43..18e1c1256 100644 --- a/crates/traits/src/lib.rs +++ b/crates/primitives/src/traits/mod.rs @@ -8,7 +8,11 @@ use ark_r1cs_std::{ fields::{fp::FpVar, FieldVar}, groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, }; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::{ + iter::Sum, + ops::{Add, Mul}, +}; pub type CF1 = ::ScalarField; pub type CF2 = <::BaseField as ArkField>::BasePrimeField; @@ -73,7 +77,7 @@ impl, const N: usize> Inputize for Fp { } } -impl> Inputize for Projective

{ +impl> Inputize for Projective

{ /// Returns the internal representation in the same order as how the value /// is allocated in `ProjectiveVar::new_input`. fn inputize(&self) -> Vec { @@ -85,7 +89,7 @@ impl> Inputize for Projective

InputizeNonNative for P { +impl InputizeNonNative for P { /// Returns the internal representation in the same order as how the value /// is allocated in `NonNativeUintVar::new_input`. fn inputize_nonnative(&self) -> Vec { @@ -97,8 +101,8 @@ impl InputizeNonNative for P { } } -impl> InputizeNonNative - for Projective

+impl> + InputizeNonNative for Projective

{ /// Returns the internal representation in the same order as how the value /// is allocated in `NonNativeAffineVar::new_input`. @@ -112,7 +116,7 @@ impl> InputizeNonNative

+ Absorb + AbsorbNonNative + Inputize { const BITS_PER_LIMB: usize; @@ -120,15 +124,15 @@ pub trait Field: type Var: FieldVar; } -impl, const N: usize> Field for Fp { +impl, const N: usize> SonobeField for Fp { const BITS_PER_LIMB: usize = 55; // TODO: make this configurable type Var = FpVar; } /// `Curve` trait is a wrapper around `CurveGroup` that also includes the /// necessary bounds for the curve to be used conveniently in folding schemes. -pub trait Curve: - CurveGroup +pub trait SonobeCurve: + CurveGroup + AbsorbNonNative + Inputize + InputizeNonNative @@ -137,7 +141,9 @@ pub trait Curve: type Var: CurveVar; } -impl> Curve for Projective

{ +impl> SonobeCurve + for Projective

+{ type Var = ProjectiveVar>; } @@ -175,6 +181,13 @@ impl AbsorbNonNative for [T] { } } +impl AbsorbNonNative for (T, T) { + fn to_native_sponge_field_elements(&self, dest: &mut Vec) { + self.0.to_native_sponge_field_elements(dest); + self.1.to_native_sponge_field_elements(dest); + } +} + impl> AbsorbNonNativeGadget for &T { fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { T::to_native_sponge_field_elements(self) @@ -208,7 +221,7 @@ impl, const N: usize> AbsorbNonNative for Fp { } } -impl> AbsorbNonNative for Projective

{ +impl> AbsorbNonNative for Projective

{ fn to_native_sponge_field_elements(&self, dest: &mut Vec) { let affine = self.into_affine(); let (x, y) = affine.xy().unwrap_or_default(); @@ -217,29 +230,83 @@ impl> AbsorbNonNative for Projective

{ } } -/// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie. -/// inside the agmented F' function). -/// The parameter z_i denotes the current state, and z_{i+1} denotes the next state after applying -/// the step. -/// Note that the external inputs for the specific circuit are defined at the implementation of -/// both `FCircuit::ExternalInputs` and `FCircuit::ExternalInputsVar`, where the `Default` trait -/// implementation for the `ExternalInputs` returns the initialized data structure (ie. if the type -/// contains a vector, it is initialized at the expected length). -pub trait FCircuit { - type ExternalInputs; - - /// returns the number of elements in the state of the FCircuit, which corresponds to the - /// FCircuit inputs. - fn state_len(&self) -> usize; - - /// generates the constraints for the step of F for the given z_i - fn generate_step_constraints( - // this method uses self, so that each FCircuit implementation (and different frontends) - // can hold a state if needed to store data to generate the constraints. - &self, - cs: ConstraintSystemRef, - i: usize, - z_i: Vec>, - external_inputs: Self::ExternalInputs, // inputs that are not part of the state - ) -> Result>, SynthesisError>; +#[derive(Clone, Copy, Default, Debug)] +pub struct Null; + +impl Add for Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Add for &Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Mul for Null { + type Output = Self; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Mul for &Null { + type Output = Null; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Sum for Null { + fn sum>(_: I) -> Self { + Null + } +} + +pub trait ScalarRLC { + type Value; + + fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value; +} + +impl ScalarRLC for I +where + I::Item: Add + Sum + for<'a> Mul<&'a Coeff, Output = I::Item>, +{ + type Value = I::Item; + + fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value { + self.zip(coeffs).map(|(v, c)| v * c).sum::() + } +} + +pub trait SliceRLC { + type Value; + + fn slice_rlc(self, coeffs: &[Coeff]) -> Vec; +} + +impl<'a, T: 'a, I: Iterator, Coeff> SliceRLC for I +where + T: Add + Copy, + for<'x> T: Mul<&'x Coeff, Output = T>, +{ + type Value = T; + + fn slice_rlc(self, coeffs: &[Coeff]) -> Vec { + let mut iter = self.zip(coeffs).map(|(v, c)| v.iter().map(|x| *x * c)); + let first = iter.next().unwrap(); + + iter.fold(first.collect(), |acc, v| { + acc.into_iter().zip(v).map(|(a, b)| a + b).collect() + }) + } } diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index a7239a11d..a4331c63e 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -3,7 +3,8 @@ use ark_ec::CurveGroup; use ark_ff::PrimeField; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; use ark_relations::gr1cs::SynthesisError; -use sonobe_traits::{AbsorbNonNative, AbsorbNonNativeGadget}; + +use crate::traits::{AbsorbNonNative, AbsorbNonNativeGadget}; pub mod poseidon; diff --git a/crates/primitives/src/transcripts/poseidon.rs b/crates/primitives/src/transcripts/poseidon.rs index 7eb281113..0d6519beb 100644 --- a/crates/primitives/src/transcripts/poseidon.rs +++ b/crates/primitives/src/transcripts/poseidon.rs @@ -150,9 +150,9 @@ pub mod tests { let config = poseidon_canonical_config::(); let mut poseidon_sponge: PoseidonSponge<_> = CryptographicSponge::new(&config); - let v: Vec = vec![1, 2, 3, 4] + let v = vec![1, 2, 3, 4] .into_iter() - .map(|x| Fr::from(x)) + .map(Fr::from) .collect::>(); poseidon_sponge.absorb(&v); poseidon_sponge.squeeze_field_elements::(1); diff --git a/crates/traits/Cargo.toml b/crates/traits/Cargo.toml deleted file mode 100644 index f2ee8164e..000000000 --- a/crates/traits/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "sonobe-traits" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -ark-ec = { workspace = true } -ark-ff = { workspace = true, features = ["asm"] } -ark-std = { workspace = true, features = ["getrandom"] } -ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } -ark-relations = { workspace = true } -ark-r1cs-std = { workspace = true } - -[features] -default = ["parallel"] -parallel = [ - "ark-relations/parallel", - "ark-r1cs-std/parallel", -] \ No newline at end of file From 5e8d3783bbf9bbba195ebb2d0d9631a16959cf5d Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 18:43:27 +0800 Subject: [PATCH 02/93] Refactor: A unifed absorb trait for native and nonnative --- crates/primitives/src/gadgets/math/mod.rs | 2 +- .../src/gadgets/nonnative/affine.rs | 5 +- crates/primitives/src/sumcheck/mod.rs | 23 ++- crates/primitives/src/traits/mod.rs | 139 +++++++++++------- crates/primitives/src/transcripts/mod.rs | 103 +++++++++---- crates/primitives/src/transcripts/poseidon.rs | 87 ++++++----- 6 files changed, 220 insertions(+), 139 deletions(-) diff --git a/crates/primitives/src/gadgets/math/mod.rs b/crates/primitives/src/gadgets/math/mod.rs index 40ed4e53e..c4e275c5a 100644 --- a/crates/primitives/src/gadgets/math/mod.rs +++ b/crates/primitives/src/gadgets/math/mod.rs @@ -1,3 +1,3 @@ pub mod eq; pub mod matrix; -pub mod vector; \ No newline at end of file +pub mod vector; diff --git a/crates/primitives/src/gadgets/nonnative/affine.rs b/crates/primitives/src/gadgets/nonnative/affine.rs index c290925d6..f24bf8968 100644 --- a/crates/primitives/src/gadgets/nonnative/affine.rs +++ b/crates/primitives/src/gadgets/nonnative/affine.rs @@ -150,7 +150,8 @@ mod tests { use ark_r1cs_std::groups::curves::short_weierstrass::ProjectiveVar; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, UniformRand}; - use crate::traits::{AbsorbNonNative, Inputize, InputizeNonNative}; + + use crate::traits::{Absorbable, Inputize, InputizeNonNative}; use super::*; @@ -173,7 +174,7 @@ mod tests { let p_var = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; assert_eq!( p_var.to_native_sponge_field_elements()?.value()?, - p.to_native_sponge_field_elements_as_vec() + p.extract_absorbed() ); Ok(()) } diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index 2868f5828..717597cbc 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -9,7 +9,6 @@ //! This module implements the sum check protocol. -use ark_crypto_primitives::sponge::Absorb; use ark_ff::PrimeField; use ark_poly::{ univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, Polynomial, @@ -19,7 +18,7 @@ use ark_std::{cfg_chunks, cfg_into_iter, cfg_iter, fmt::Debug}; use rayon::prelude::*; use thiserror::Error; -use crate::transcripts::Transcript; +use crate::{traits::Absorbable, transcripts::Transcript}; use utils::{ barycentric_weights, compute_lagrange_interpolated_poly, extrapolate, VPAuxInfo, @@ -65,12 +64,12 @@ pub struct SumCheckSubClaim { pub struct IOPSumCheck; impl IOPSumCheck { - pub fn prove( + pub fn prove>( mut poly: VirtualPolynomial, transcript: &mut impl Transcript, ) -> Result<(IOPProof, Vec>), Error> { - transcript.absorb(&F::from(poly.aux_info.num_variables as u64)); - transcript.absorb(&F::from(poly.aux_info.max_degree as u64)); + transcript.add(&F::from(poly.aux_info.num_variables as u64)); + transcript.add(&F::from(poly.aux_info.max_degree as u64)); let extrapolation_aux = (1..poly.aux_info.max_degree) .map(|degree| { let points = (0..1 + degree as u64).map(F::from).collect::>(); @@ -161,10 +160,10 @@ impl IOPSumCheck { }); let prover_poly = compute_lagrange_interpolated_poly(&products_sum).coeffs; - transcript.absorb(&prover_poly); + transcript.add(&prover_poly); prover_msgs.push(prover_poly); - let challenge = transcript.get_challenge(); + let challenge = transcript.challenge_field_element(); challenges.push(challenge); poly.flattened_ml_extensions.iter_mut().for_each(|mle| { mle.evaluations = cfg_chunks!(mle.evaluations, 2) @@ -183,22 +182,22 @@ impl IOPSumCheck { )) } - pub fn verify( + pub fn verify>( claimed_sum: F, proof: &IOPProof, aux_info: &VPAuxInfo, transcript: &mut impl Transcript, ) -> Result, Error> { - transcript.absorb(&F::from(aux_info.num_variables as u64)); - transcript.absorb(&F::from(aux_info.max_degree as u64)); + transcript.add(&F::from(aux_info.num_variables as u64)); + transcript.add(&F::from(aux_info.max_degree as u64)); assert_eq!(aux_info.num_variables, proof.proofs.len()); let challenges = proof .proofs .iter() .map(|msg| { - transcript.absorb(&msg); - transcript.get_challenge() + transcript.add(msg); + transcript.challenge_field_element() }) .collect::>(); diff --git a/crates/primitives/src/traits/mod.rs b/crates/primitives/src/traits/mod.rs index 18e1c1256..aecf72258 100644 --- a/crates/primitives/src/traits/mod.rs +++ b/crates/primitives/src/traits/mod.rs @@ -1,4 +1,3 @@ -use ark_crypto_primitives::sponge::Absorb; use ark_ec::{ short_weierstrass::{Projective, SWCurveConfig}, AffineRepr, CurveGroup, PrimeGroup, @@ -10,7 +9,9 @@ use ark_r1cs_std::{ }; use ark_relations::gr1cs::SynthesisError; use ark_std::{ + any::TypeId, iter::Sum, + mem::transmute_copy, ops::{Add, Mul}, }; @@ -117,7 +118,7 @@ impl> /// `Field` trait is a wrapper around `PrimeField` that also includes the /// necessary bounds for the field to be used conveniently in folding schemes. pub trait SonobeField: - PrimeField + Absorb + AbsorbNonNative + Inputize + PrimeField + Absorbable + Inputize { const BITS_PER_LIMB: usize; /// The in-circuit variable type for this field. @@ -133,7 +134,7 @@ impl, const N: usize> SonobeField for Fp { /// necessary bounds for the curve to be used conveniently in folding schemes. pub trait SonobeCurve: CurveGroup - + AbsorbNonNative + + Absorbable + Inputize + InputizeNonNative { @@ -147,47 +148,105 @@ impl> SonobeC type Var = ProjectiveVar>; } -/// An interface for objects that can be absorbed by a `Transcript`. -/// -/// Matches `Absorb` in `ark-crypto-primitives`. -pub trait AbsorbNonNative { - /// Converts the object into field elements that can be absorbed by a `Transcript`. +pub trait Absorbable { + /// Converts the object into field elements that can be absorbed by a `CryptographicSponge`. /// Append the list to `dest` - fn to_native_sponge_field_elements(&self, dest: &mut Vec); + fn absorb_into(&self, dest: &mut Vec); - /// Converts the object into field elements that can be absorbed by a `Transcript`. + /// Converts the object into field elements that can be absorbed by a `CryptographicSponge`. /// Return the list as `Vec` - fn to_native_sponge_field_elements_as_vec(&self) -> Vec { + fn extract_absorbed(&self) -> Vec { let mut result = Vec::new(); - self.to_native_sponge_field_elements(&mut result); + self.absorb_into(&mut result); result } } -/// An interface for objects that can be absorbed by a `TranscriptVar` whose constraint field -/// is `F`. -/// -/// Matches `AbsorbGadget` in `ark-crypto-primitives`. -pub trait AbsorbNonNativeGadget { - /// Converts the object into field elements that can be absorbed by a `TranscriptVar`. - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError>; +impl, const N: usize> Absorbable for Fp { + fn absorb_into(&self, dest: &mut Vec) { + if TypeId::of::() == TypeId::of::() { + // Safe because `F` and `Self` have the same type + // TODO (@winderica): specialization when??? + dest.push(unsafe { transmute_copy::(self) }); + } else { + let bits_per_limb = F::MODULUS_BIT_SIZE - 1; + let num_limbs = Self::MODULUS_BIT_SIZE.div_ceil(bits_per_limb); + + let mut limbs = self + .into_bigint() + .to_bits_le() + .chunks(bits_per_limb as usize) + .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) + .collect::>(); + limbs.resize(num_limbs as usize, F::zero()); + + dest.extend(&limbs) + } + } } -impl AbsorbNonNative for [T] { - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - for t in self.iter() { - t.to_native_sponge_field_elements(dest); +impl>> Absorbable for Projective

{ + fn absorb_into(&self, dest: &mut Vec) { + let affine = self.into_affine(); + let (x, y) = affine.xy().unwrap_or_default(); + [x, y].absorb_into(dest); + } +} + +impl Absorbable for usize { + fn absorb_into(&self, dest: &mut Vec) { + dest.push(F::from(*self as u64)); + } +} + +impl> Absorbable for &T { + fn absorb_into(&self, dest: &mut Vec) { + >::absorb_into(self, dest); + } +} + +impl> Absorbable for (T, T) { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest); + self.1.absorb_into(dest); + } +} + +impl + 'static> Absorbable for [T] { + fn absorb_into(&self, dest: &mut Vec) { + if TypeId::of::() == TypeId::of::() { + // Safe because `F` and `T` have the same type + dest.extend(unsafe { transmute_copy::<&[T], &[F]>(&self) }); + } else { + for t in self.iter() { + t.absorb_into(dest); + } } } } -impl AbsorbNonNative for (T, T) { - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - self.0.to_native_sponge_field_elements(dest); - self.1.to_native_sponge_field_elements(dest); +impl + 'static, const N: usize> Absorbable for [T; N] { + fn absorb_into(&self, dest: &mut Vec) { + <[T] as Absorbable>::absorb_into(self, dest); } } +impl + 'static> Absorbable for Vec { + fn absorb_into(&self, dest: &mut Vec) { + <[T] as Absorbable>::absorb_into(self, dest); + } +} + +// TODO: rework this +/// An interface for objects that can be absorbed by a `TranscriptVar` whose constraint field +/// is `F`. +/// +/// Matches `AbsorbGadget` in `ark-crypto-primitives`. +pub trait AbsorbNonNativeGadget { + /// Converts the object into field elements that can be absorbed by a `TranscriptVar`. + fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError>; +} + impl> AbsorbNonNativeGadget for &T { fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { T::to_native_sponge_field_elements(self) @@ -204,32 +263,6 @@ impl> AbsorbNonNativeGadget for [T } } -impl, const N: usize> AbsorbNonNative for Fp { - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; - let num_limbs = (Fp::::MODULUS_BIT_SIZE as usize).div_ceil(bits_per_limb); - - let mut limbs = self - .into_bigint() - .to_bits_le() - .chunks(bits_per_limb) - .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) - .collect::>(); - limbs.resize(num_limbs, F::zero()); - - dest.extend(&limbs) - } -} - -impl> AbsorbNonNative for Projective

{ - fn to_native_sponge_field_elements(&self, dest: &mut Vec) { - let affine = self.into_affine(); - let (x, y) = affine.xy().unwrap_or_default(); - - [x, y].to_native_sponge_field_elements(dest); - } -} - #[derive(Clone, Copy, Default, Debug)] pub struct Null; diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index a4331c63e..504559a7e 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -1,46 +1,85 @@ -use ark_crypto_primitives::sponge::{constraints::CryptographicSpongeVar, CryptographicSponge}; +use ark_crypto_primitives::sponge::{ + constraints::CryptographicSpongeVar, CryptographicSponge, FieldElementSize, +}; use ark_ec::CurveGroup; -use ark_ff::PrimeField; +use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; use ark_relations::gr1cs::SynthesisError; -use crate::traits::{AbsorbNonNative, AbsorbNonNativeGadget}; +use crate::traits::{AbsorbNonNativeGadget, Absorbable}; pub mod poseidon; -pub trait Transcript: CryptographicSponge { +pub trait Transcript { /// `new_with_pp_hash` creates a new transcript / sponge with the given /// hash of the public parameters. - fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self; + fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self + where + F: Absorbable, + Self: CryptographicSponge, + { + let mut sponge = Self::new(config); + sponge.add(&pp_hash); + sponge + } - /// `absorb_point` is for absorbing points whose `BaseField` is the field of - /// the sponge, i.e., the type `C` of these points should satisfy - /// `C::BaseField = F`. - /// - /// If the sponge field `F` is `C::ScalarField`, call `absorb_nonnative` - /// instead. - fn absorb_point>(&mut self, v: &C); - /// `absorb_nonnative` is for structs that contain non-native (field or - /// group) elements, including: - /// - /// - A field element of type `T: PrimeField` that will be absorbed into a - /// sponge that operates in another field `F != T`. - /// - A group element of type `C: CurveGroup` that will be absorbed into a - /// sponge that operates in another field `F != C::BaseField`, e.g., - /// `F = C::ScalarField`. - /// - A `CommittedInstance` on the secondary curve (used for CycleFold) that - /// will be absorbed into a sponge that operates in the (scalar field of - /// the) primary curve. - /// - /// Note that although a `CommittedInstance` for `AugmentedFCircuit` on - /// the primary curve also contains non-native elements, we still regard - /// it as native, because the sponge is on the same curve. - fn absorb_nonnative(&mut self, v: &V); + fn add + ?Sized>(&mut self, input: &A); + + /// Squeeze `num_bytes` bytes from the sponge. + fn get_bytes(&mut self, num_bytes: usize) -> Vec; + + /// Squeeze `num_bits` bits from the sponge. + fn get_bits(&mut self, num_bits: usize) -> Vec; + + fn get_field_elements_with_sizes(&mut self, sizes: &[FieldElementSize]) -> Vec; + + fn get_field_elements(&mut self, num_elements: usize) -> Vec; + + /// Creates a new sponge with applied domain separation. + fn separate_domain(&self, domain: &[u8]) -> Self + where + F: Absorbable, + Self: CryptographicSponge, + { + let mut new_sponge = self.clone(); + + let mut input = domain.len().to_le_bytes().to_vec(); + input.extend_from_slice(domain); + + let limbs = input + .chunks(F::MODULUS_BIT_SIZE.div_ceil(8) as usize) + .map(|chunk| F::from_le_bytes_mod_order(chunk)) + .collect::>(); + + new_sponge.add(&limbs); + + new_sponge + } - fn get_challenge(&mut self) -> F; - /// get_challenge_nbits returns a field element of size nbits - fn get_challenge_nbits(&mut self, nbits: usize) -> Vec; - fn get_challenges(&mut self, n: usize) -> Vec; + fn challenge_field_element(&mut self) -> F + where + F: Absorbable, + { + let c = self.get_field_elements(1); + self.add(&c[0]); + c[0] + } + fn challenge_bits(&mut self, nbits: usize) -> Vec + where + F: Absorbable, + { + let bits = self.get_bits(nbits); + self.add(&F::from(F::BigInt::from_bits_le(&bits))); + bits + } + fn challenge_field_elements(&mut self, n: usize) -> Vec + where + F: Absorbable, + { + let c = self.get_field_elements(n); + self.add(&c); + c + } } pub trait TranscriptVar: diff --git a/crates/primitives/src/transcripts/poseidon.rs b/crates/primitives/src/transcripts/poseidon.rs index 0d6519beb..a3a298108 100644 --- a/crates/primitives/src/transcripts/poseidon.rs +++ b/crates/primitives/src/transcripts/poseidon.rs @@ -1,47 +1,56 @@ +use std::mem::transmute_copy; + use ark_crypto_primitives::sponge::{ constraints::CryptographicSpongeVar, poseidon::{ constraints::PoseidonSpongeVar, find_poseidon_ark_and_mds, PoseidonConfig, PoseidonSponge, }, - Absorb, CryptographicSponge, + Absorb, CryptographicSponge, FieldBasedCryptographicSponge, }; -use ark_ec::{AffineRepr, CurveGroup}; -use ark_ff::{BigInteger, PrimeField}; +use ark_ec::CurveGroup; +use ark_ff::PrimeField; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; -use super::{AbsorbNonNative, AbsorbNonNativeGadget, Transcript, TranscriptVar}; - -impl Transcript for PoseidonSponge { - fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self { - let mut sponge = Self::new(config); - sponge.absorb(&pp_hash); - sponge +use crate::transcripts::{Absorbable, FieldElementSize}; + +use super::{AbsorbNonNativeGadget, Transcript, TranscriptVar}; + +impl Transcript for PoseidonSponge { + fn add + ?Sized>(&mut self, input: &A) { + struct Hack(I); + impl Absorb for Hack> { + fn to_sponge_bytes(&self, _: &mut Vec) { + // Unreachable because `PoseidonSponge::absorb` only calls + // `to_sponge_field_elements_as_vec::` + unreachable!() + } + + fn to_sponge_field_elements(&self, dest: &mut Vec) { + // Safe because `F` in `to_sponge_field_elements_as_vec::`, + // which is called by `PoseidonSponge::absorb`, is the same as + // `T` here. + dest.extend(unsafe { transmute_copy::<&[F], &[T]>(&self.0.as_ref()) }); + } + } + let v = input.extract_absorbed(); + CryptographicSponge::absorb(self, &Hack(v)); } - // Compatible with the in-circuit `TranscriptVar::absorb_point` - fn absorb_point>(&mut self, p: &C) { - let (x, y) = p.into_affine().xy().unwrap_or_default(); - self.absorb(&x); - self.absorb(&y); - } - fn absorb_nonnative(&mut self, v: &V) { - self.absorb(&v.to_native_sponge_field_elements_as_vec::()); + fn get_bits(&mut self, num_bits: usize) -> Vec { + CryptographicSponge::squeeze_bits(self, num_bits) } - fn get_challenge(&mut self) -> F { - let c = self.squeeze_field_elements(1); - self.absorb(&c[0]); - c[0] + + fn get_bytes(&mut self, num_bytes: usize) -> Vec { + CryptographicSponge::squeeze_bytes(self, num_bytes) } - fn get_challenge_nbits(&mut self, nbits: usize) -> Vec { - let bits = self.squeeze_bits(nbits); - self.absorb(&F::from(F::BigInt::from_bits_le(&bits))); - bits + + fn get_field_elements_with_sizes(&mut self, sizes: &[FieldElementSize]) -> Vec { + self.squeeze_native_field_elements_with_sizes(sizes) } - fn get_challenges(&mut self, n: usize) -> Vec { - let c = self.squeeze_field_elements(n); - self.absorb(&c); - c + + fn get_field_elements(&mut self, num_elements: usize) -> Vec { + self.squeeze_native_field_elements(num_elements) } } @@ -133,7 +142,7 @@ pub fn poseidon_canonical_config() -> PoseidonConfig { pub mod tests { use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; use ark_ec::PrimeGroup; - use ark_ff::UniformRand; + use ark_ff::{BigInteger, UniformRand}; use ark_r1cs_std::{ alloc::AllocVar, groups::curves::short_weierstrass::ProjectiveVar, GR1CSVar, }; @@ -174,8 +183,8 @@ pub mod tests { let rng = &mut test_rng(); let p = G1::rand(rng); - tr.absorb_point(&p); - let c = tr.get_challenge(); + tr.add(&p); + let c = tr.challenge_field_element(); // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); @@ -200,8 +209,8 @@ pub mod tests { let rng = &mut test_rng(); let p = G1::rand(rng); - tr.absorb_nonnative(&p); - let c = tr.get_challenge(); + tr.add(&p); + let c = tr.challenge_field_element(); // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); @@ -221,8 +230,8 @@ pub mod tests { // use 'native' transcript let config = poseidon_canonical_config::(); let mut tr = PoseidonSponge::::new(&config); - tr.absorb(&Fr::from(42_u32)); - let c = tr.get_challenge(); + tr.add(&Fr::from(42_u32)); + let c = tr.challenge_field_element(); // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); @@ -243,10 +252,10 @@ pub mod tests { // use 'native' transcript let config = poseidon_canonical_config::(); let mut tr = PoseidonSponge::::new(&config); - tr.absorb(&Fq::from(42_u32)); + tr.add(&Fq::from(42_u32)); // get challenge from native transcript - let c_bits = tr.get_challenge_nbits(nbits); + let c_bits = tr.challenge_bits(nbits); // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); From 4d01dfe3caa8ecd9f0ce893f01e4033ce1e1afd9 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 18:43:52 +0800 Subject: [PATCH 03/93] Refactor: move `FCircuit`'s field bound to associated type --- crates/primitives/src/circuits/mod.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index 0a0069ea1..1e8f34fb8 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -3,7 +3,10 @@ use ark_r1cs_std::fields::fp::FpVar; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, }; -use ark_std::{marker::PhantomData, ops::{Index, IndexMut}}; +use ark_std::{ + marker::PhantomData, + ops::{Index, IndexMut}, +}; pub mod utils; @@ -15,7 +18,8 @@ pub mod utils; /// both `FCircuit::ExternalInputs` and `FCircuit::ExternalInputsVar`, where the `Default` trait /// implementation for the `ExternalInputs` returns the initialized data structure (ie. if the type /// contains a vector, it is initialized at the expected length). -pub trait FCircuit { +pub trait FCircuit { + type Field: PrimeField; type ExternalInputs; /// returns the number of elements in the state of the FCircuit, which corresponds to the @@ -27,11 +31,11 @@ pub trait FCircuit { // this method uses self, so that each FCircuit implementation (and different frontends) // can hold a state if needed to store data to generate the constraints. &self, - cs: ConstraintSystemRef, + cs: ConstraintSystemRef, i: usize, - z_i: Vec>, + z_i: Vec>, external_inputs: Self::ExternalInputs, // inputs that are not part of the state - ) -> Result>, SynthesisError>; + ) -> Result>, SynthesisError>; } #[derive(Clone, Debug, PartialEq)] From 644d11c82b385cfc3b16118dd4480cbc17e578da Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 24 Oct 2025 20:49:56 +0800 Subject: [PATCH 04/93] Refactor: move various traits to their dedicated submodules --- crates/primitives/Cargo.toml | 1 + crates/primitives/src/algebra/field/mod.rs | 95 ++ .../primitives/src/algebra/field/nonnative.rs | 1234 +++++++++++++++++ .../src/algebra/field/nonnative2.rs | 1159 ++++++++++++++++ crates/primitives/src/algebra/group/mod.rs | 174 +++ .../affine.rs => algebra/group/nonnative.rs} | 76 +- crates/primitives/src/algebra/mod.rs | 3 + crates/primitives/src/algebra/ops/bits.rs | 33 + .../src/{gadgets/math => algebra/ops}/eq.rs | 0 .../{gadgets/math => algebra/ops}/matrix.rs | 0 .../src/{gadgets/math => algebra/ops}/mod.rs | 2 + crates/primitives/src/algebra/ops/rlc.rs | 44 + .../{gadgets/math => algebra/ops}/vector.rs | 0 .../src/arithmetizations/ccs/circuits.rs | 2 +- .../src/arithmetizations/ccs/mod.rs | 7 +- .../src/arithmetizations/r1cs/circuits.rs | 6 +- crates/primitives/src/commitments/mod.rs | 76 +- crates/primitives/src/commitments/pedersen.rs | 182 ++- crates/primitives/src/gadgets/mod.rs | 2 - .../primitives/src/gadgets/nonnative/mod.rs | 2 - .../primitives/src/gadgets/nonnative/uint.rs | 993 ------------- crates/primitives/src/lib.rs | 2 +- crates/primitives/src/sumcheck/mod.rs | 2 +- crates/primitives/src/traits.rs | 56 + crates/primitives/src/traits/mod.rs | 345 ----- .../primitives/src/transcripts/absorbable.rs | 98 ++ crates/primitives/src/transcripts/mod.rs | 108 +- crates/primitives/src/transcripts/poseidon.rs | 88 +- 28 files changed, 3256 insertions(+), 1534 deletions(-) create mode 100644 crates/primitives/src/algebra/field/mod.rs create mode 100644 crates/primitives/src/algebra/field/nonnative.rs create mode 100644 crates/primitives/src/algebra/field/nonnative2.rs create mode 100644 crates/primitives/src/algebra/group/mod.rs rename crates/primitives/src/{gadgets/nonnative/affine.rs => algebra/group/nonnative.rs} (74%) create mode 100644 crates/primitives/src/algebra/mod.rs create mode 100644 crates/primitives/src/algebra/ops/bits.rs rename crates/primitives/src/{gadgets/math => algebra/ops}/eq.rs (100%) rename crates/primitives/src/{gadgets/math => algebra/ops}/matrix.rs (100%) rename crates/primitives/src/{gadgets/math => algebra/ops}/mod.rs (61%) create mode 100644 crates/primitives/src/algebra/ops/rlc.rs rename crates/primitives/src/{gadgets/math => algebra/ops}/vector.rs (100%) delete mode 100644 crates/primitives/src/gadgets/mod.rs delete mode 100644 crates/primitives/src/gadgets/nonnative/mod.rs delete mode 100644 crates/primitives/src/gadgets/nonnative/uint.rs create mode 100644 crates/primitives/src/traits.rs delete mode 100644 crates/primitives/src/traits/mod.rs create mode 100644 crates/primitives/src/transcripts/absorbable.rs diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index efea822bc..7bdb8fc6f 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -17,6 +17,7 @@ ark-r1cs-std = { workspace = true } ark-serialize = { workspace = true } num-bigint = { workspace = true, features = ["rand"] } num-integer = { workspace = true } +num-traits = { workspace = true } rayon = { workspace = true } thiserror = { workspace = true } diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs new file mode 100644 index 000000000..4f836b807 --- /dev/null +++ b/crates/primitives/src/algebra/field/mod.rs @@ -0,0 +1,95 @@ +use ark_ff::{BigInteger, Fp, FpConfig, PrimeField}; +use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::{any::TypeId, mem::transmute_copy}; + +use crate::{ + traits::{Inputize, InputizeNonNative}, + transcripts::{Absorbable, AbsorbableGadget}, +}; + +pub mod nonnative; +pub mod nonnative2; + +/// `Field` trait is a wrapper around `PrimeField` that also includes the +/// necessary bounds for the field to be used conveniently in folding schemes. +pub trait SonobeField: + PrimeField + Absorbable + Inputize +{ + const BITS_PER_LIMB: usize; + /// The in-circuit variable type for this field. + type Var: FieldVar; +} + +impl, const N: usize> SonobeField for Fp { + // For a `F` with order > 250 bits, 55 is chosen for optimizing the most + // expensive part `Az∘Bz` when checking the R1CS relation for CycleFold. + // Consider using `NonNativeUintVar` to represent the base field `Fq`. + // Since 250 / 55 = 4.46, the `NonNativeUintVar` has 5 limbs. + // Now, the multiplication of two `NonNativeUintVar`s has 9 limbs, and + // each limb has at most 2^{55 * 2} * 5 = 112.3 bits. + // For a 1400x1400 matrix `A`, the multiplication of `A`'s row and `z` + // is the sum of 1400 `NonNativeUintVar`s, each with 9 limbs. + // Thus, the maximum bit length of limbs of each element in `Az` is + // 2^{55 * 2} * 5 * 1400 = 122.7 bits. + // Finally, in the hadamard product of `Az` and `Bz`, every element has + // 17 limbs, whose maximum bit length is (2^{55 * 2} * 5 * 1400)^2 * 9 + // = 248.7 bits and is less than the native field `Fr`. + // Thus, 55 allows us to compute `Az∘Bz` without the expensive alignment + // operation. + // + // TODO: either make it a global const, or compute an optimal value + // based on the modulus size. + const BITS_PER_LIMB: usize = 55; // TODO: make this configurable + type Var = FpVar; +} + +impl, const N: usize> Absorbable for Fp { + fn absorb_into(&self, dest: &mut Vec) { + if TypeId::of::() == TypeId::of::() { + // Safe because `F` and `Self` have the same type + // TODO (@winderica): specialization when??? + dest.push(unsafe { transmute_copy::(self) }); + } else { + let bits_per_limb = F::MODULUS_BIT_SIZE - 1; + let num_limbs = Self::MODULUS_BIT_SIZE.div_ceil(bits_per_limb); + + let mut limbs = self + .into_bigint() + .to_bits_le() + .chunks(bits_per_limb as usize) + .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) + .collect::>(); + limbs.resize(num_limbs as usize, F::zero()); + + dest.extend(&limbs) + } + } +} + +impl AbsorbableGadget> for FpVar { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + dest.push(self.clone()); + Ok(()) + } +} + +impl, const N: usize> Inputize for Fp { + /// Returns the internal representation in the same order as how the value + /// is allocated in `FpVar::new_input`. + fn inputize(&self) -> Vec { + vec![*self] + } +} + +impl InputizeNonNative for P { + /// Returns the internal representation in the same order as how the value + /// is allocated in `NonNativeUintVar::new_input`. + fn inputize_nonnative(&self) -> Vec { + self.into_bigint() + .to_bits_le() + .chunks(F::BITS_PER_LIMB) + .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) + .collect() + } +} diff --git a/crates/primitives/src/algebra/field/nonnative.rs b/crates/primitives/src/algebra/field/nonnative.rs new file mode 100644 index 000000000..ef051fef1 --- /dev/null +++ b/crates/primitives/src/algebra/field/nonnative.rs @@ -0,0 +1,1234 @@ +use ark_ff::{BigInteger, One, PrimeField, Zero}; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + convert::ToBitsGadget, + fields::{fp::FpVar, FieldVar}, + prelude::EqGadget, + select::CondSelectGadget, + GR1CSVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + cmp::{max, min}, + ops::Index, +}; +use num_bigint::{BigInt, BigUint, Sign}; +use num_integer::Integer; +use num_traits::Signed; + +use crate::algebra::ops::bits::{FromBitsGadget, ToBitsGadgetExt}; +use crate::{ + algebra::{ + field::SonobeField, + ops::{ + eq::EquivalenceGadget, + matrix::{MatrixGadget, SparseMatrixVar}, + vector::VectorGadget, + }, + }, + transcripts::AbsorbableGadget, +}; + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Bound { + pub lb: BigInt, + pub ub: BigInt, +} + +impl Bound { + pub fn zero() -> Self { + Self::default() + } + + pub fn new_ub(ub: BigInt) -> Self { + Self { + lb: BigInt::zero(), + ub, + } + } + + pub fn new_n_bits(n: usize) -> Self { + Self { + lb: BigInt::zero(), + ub: (BigInt::one() << n) - BigInt::one(), + } + } + + pub fn new(lb: BigInt, ub: BigInt) -> Self { + Self { lb, ub } + } +} + +impl Bound { + pub fn add(&self, other: &Self) -> Self { + Self { + lb: &self.lb + &other.lb, + ub: &self.ub + &other.ub, + } + } + + pub fn sub(&self, other: &Self) -> Self { + Self { + lb: &self.lb - &other.ub, + ub: &self.ub - &other.lb, + } + } + + pub fn add_many(limbs: &[Self]) -> Self { + Self { + lb: limbs.iter().map(|l| &l.lb).sum(), + ub: limbs.iter().map(|l| &l.ub).sum(), + } + } + + pub fn mul(&self, other: &Self) -> Self { + let ll = &self.lb * &other.lb; + let lu = &self.lb * &other.ub; + let ul = &self.ub * &other.lb; + let uu = &self.ub * &other.ub; + + Self { + lb: min(min(&ll, &lu), min(&ul, &uu)).clone(), + ub: max(max(&ll, &lu), max(&ul, &uu)).clone(), + } + } + + pub fn shl(&self, shift: usize) -> Self { + Self { + lb: &self.lb << shift, + ub: &self.ub << shift, + } + } + + pub fn filter_safe(self) -> Option { + let limit = BigInt::from_biguint(Sign::Plus, F::MODULUS_MINUS_ONE_DIV_TWO.into()); + (self.ub <= limit && self.lb >= -limit).then_some(self) + } +} + +// /// `LimbVar` represents a single limb of a non-native unsigned integer in the +// /// circuit. +// /// The limb value `v` should be small enough to fit into `FpVar`, and we also +// /// store an upper bound `ub` for the limb value, which is treated as a constant +// /// in the circuit and is used for efficient equality checks and some arithmetic +// /// operations. +// #[derive(Debug, Clone)] +// pub struct LimbVar { +// pub v: FpVar, +// pub lb: BigInt, +// pub ub: BigInt, +// } + +// impl]>> From for LimbVar { +// fn from(bits: B) -> Self { +// Self { +// // `Boolean::le_bits_to_fp` will return an error if the internal +// // invocation of `Boolean::enforce_in_field_le` fails. +// // However, this method is only called when the length of `bits` is +// // greater than `F::MODULUS_BIT_SIZE`, which should not happen in +// // our case where `bits` is guaranteed to be short. +// v: Boolean::le_bits_to_fp(bits.as_ref()).unwrap(), +// lb: BigInt::zero(), +// ub: (BigInt::one() << bits.as_ref().len()) - BigInt::one(), +// } +// } +// } + +// impl Default for LimbVar { +// fn default() -> Self { +// Self { +// v: FpVar::zero(), +// lb: BigInt::zero(), +// ub: BigInt::zero(), +// } +// } +// } + +// impl GR1CSVar for LimbVar { +// type Value = F; + +// fn cs(&self) -> ConstraintSystemRef { +// self.v.cs() +// } + +// fn value(&self) -> Result { +// self.v.value() +// } +// } + +// impl CondSelectGadget for LimbVar { +// fn conditionally_select( +// cond: &Boolean, +// true_value: &Self, +// false_value: &Self, +// ) -> Result { +// // We only allow selecting between two values with the same upper bound +// assert_eq!(true_value.lb, false_value.lb); +// assert_eq!(true_value.ub, false_value.ub); +// Ok(Self { +// v: cond.select(&true_value.v, &false_value.v)?, +// lb: true_value.lb.clone(), +// ub: true_value.ub.clone(), +// }) +// } +// } + +// impl LimbVar { +// /// Add two `LimbVar`s. +// /// Returns `None` if the upper bound of the sum is too large, i.e., +// /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. +// /// Otherwise, returns the sum as a `LimbVar`. +// pub fn add(&self, other: &Self) -> Option { +// let lbound = &self.lb + &other.lb; +// let ubound = &self.ub + &other.ub; +// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); +// if ubound > limit || lbound < -limit { +// None +// } else { +// Some(Self { +// v: &self.v + &other.v, +// lb: lbound, +// ub: ubound, +// }) +// } +// } + +// /// Add multiple `LimbVar`s. +// /// Returns `None` if the upper bound of the sum is too large, i.e., +// /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. +// /// Otherwise, returns the sum as a `LimbVar`. +// pub fn add_many(limbs: &[Self]) -> Option { +// let lbound = limbs.iter().map(|l| &l.lb).sum::(); +// let ubound = limbs.iter().map(|l| &l.ub).sum::(); +// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); +// if ubound > limit || lbound < -limit { +// None +// } else { +// Some(Self { +// v: if limbs.is_constant() { +// FpVar::constant(limbs.value().unwrap_or_default().into_iter().sum()) +// } else { +// limbs.iter().map(|l| &l.v).sum() +// }, +// lb: lbound, +// ub: ubound, +// }) +// } +// } + +// /// Multiply two `LimbVar`s. +// /// Returns `None` if the upper bound of the product is too large, i.e., +// /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. +// /// Otherwise, returns the product as a `LimbVar`. +// pub fn mul(&self, other: &Self) -> Option { +// let ll = &self.lb * &other.lb; +// let lu = &self.lb * &other.ub; +// let ul = &self.ub * &other.lb; +// let uu = &self.ub * &other.ub; + +// let lbound = min(min(&ll, &lu), min(&ul, &uu)).clone(); +// let ubound = max(max(&ll, &lu), max(&ul, &uu)).clone(); +// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); +// if ubound > limit || lbound < -limit { +// None +// } else { +// Some(Self { +// v: &self.v * &other.v, +// lb: lbound, +// ub: ubound, +// }) +// } +// } + +// pub fn zero() -> Self { +// Self::default() +// } + +// pub fn constant(v: BigInt) -> Self { +// let (v_sign, v_abs) = v.clone().into_parts(); +// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); +// assert!(v_abs <= limit); +// Self { +// v: if v_sign == Sign::Minus { +// FpVar::constant(-F::from(v_abs)) +// } else { +// FpVar::constant(F::from(v_abs)) +// }, +// lb: v.clone(), +// ub: v, +// } +// } +// } + +// impl ToBitsGadget for LimbVar { +// fn to_bits_le(&self) -> Result>, SynthesisError> { +// let cs = self.cs(); + +// assert_eq!(self.lb, BigInt::zero()); + +// let bits = &self +// .v +// .value() +// .unwrap_or_default() +// .into_bigint() +// .to_bits_le()[..self.ub.bits() as usize]; +// let bits = if cs.is_none() { +// Vec::new_constant(cs, bits)? +// } else { +// Vec::new_witness(cs, || Ok(bits))? +// }; + +// Boolean::le_bits_to_fp(&bits)?.enforce_equal(&self.v)?; + +// Ok(bits) +// } +// } + +/// `NonNativeUintVar` represents a non-native unsigned integer (BigUint) in the +/// circuit. +/// We apply [xJsnark](https://akosba.github.io/papers/xjsnark.pdf)'s techniques +/// for efficient operations on `NonNativeUintVar`. +/// Note that `NonNativeUintVar` is different from arkworks' `NonNativeFieldVar` +/// in that the latter runs the expensive `reduce` (`align` + `modulo` in our +/// terminology) after each arithmetic operation, while the former only reduces +/// the integer when explicitly called. +#[derive(Debug, Clone)] +pub struct NonNativeUintVar { + pub(crate) limbs: Vec>, + bounds: Vec, +} + +impl AllocVar<(BigInt, Bound), F> for NonNativeUintVar { + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + todo!(); + let cs = cs.into().cs(); + let v = f()?; + let (x, b) = v.borrow(); + + if x > &b.ub || x < &b.lb { + return Err(SynthesisError::DivisionByZero); + } + + let (x_sign, mut x_abs) = x.clone().into_parts(); + + let mut limbs = vec![]; + let mut bounds = vec![]; + + let l = max(b.lb.bits(), b.ub.bits()); + + if l == 0 { + return Ok(Self { limbs, bounds }); + } + + let (num_full_chunks, final_chunk_size) = (l as usize).div_rem(&F::BITS_PER_LIMB); + + let mask = (BigUint::one() << F::BITS_PER_LIMB) - BigUint::one(); + + loop { + let limb = FpVar::new_variable(cs.clone(), || Ok(F::from(&x_abs & &mask)), mode)?; + Self::enforce_bit_length(&limb, F::BITS_PER_LIMB)?; + limbs.push(limb); + bounds.push(Bound::new_n_bits(F::BITS_PER_LIMB)); + x_abs >>= F::BITS_PER_LIMB; + if x_abs.is_zero() { + let is_neg = Boolean::new_variable(cs.clone(), || Ok(x_sign == Sign::Minus), { + if b.lb >= BigInt::zero() || b.ub <= BigInt::zero() { + AllocationMode::Constant + } else { + mode + } + })?; + *limbs.last_mut().unwrap() *= + is_neg.select(&FpVar::one().negate()?, &FpVar::one())?; + break; + } + } + + if final_chunk_size > 0 { + let limb = FpVar::new_variable(cs.clone(), || Ok(F::from(x_abs)), mode)?; + Self::enforce_bit_length(&limb, F::BITS_PER_LIMB)?; + } + + for chunk in (0..l) + .map(|i| x.bit(i)) + .collect::>() + .chunks(F::BITS_PER_LIMB) + { + let limb = F::from(F::BigInt::from_bits_le(chunk)); + let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; + Self::enforce_bit_length(&limb, chunk.len())?; + limbs.push(limb); + bounds.push(Bound::new_n_bits(chunk.len())); + } + let s = Boolean::new_variable(cs.clone(), || Ok(x.sign() == Sign::Minus), { + if b.lb >= BigInt::zero() || b.ub <= BigInt::zero() { + AllocationMode::Constant + } else { + mode + } + })?; + limbs[num_full_chunks] = + s.select(&limbs[num_full_chunks].negate()?, &limbs[num_full_chunks])?; + + let t = BigInt::one() << ((bounds.len() - 1) * F::BITS_PER_LIMB); + bounds[num_full_chunks] = Bound::new(b.lb.div_floor(&t), b.ub.div_ceil(&t)); + + Ok(Self { limbs, bounds }) + } +} + +impl AllocVar for NonNativeUintVar { + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + + let v = BigInt::from_biguint(Sign::Plus, v.borrow().clone().into()); + let m = BigInt::from_biguint(Sign::Plus, G::MODULUS.into()); + + match mode { + AllocationMode::Constant => Self::constant(v), + _ => Self::new_variable(cs, || Ok((v, Bound::new(BigInt::zero(), m))), mode), + } + } +} + +impl EqGadget for NonNativeUintVar { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { + let mut result = Boolean::TRUE; + if self.limbs.len() != other.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.bounds.len() != other.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + for i in 0..self.limbs.len() { + if self.bounds[i] != other.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + result &= self.limbs[i].is_eq(&other.limbs[i])?; + } + Ok(result) + } + + fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { + if self.limbs.len() != other.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.bounds.len() != other.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + for i in 0..self.limbs.len() { + if self.bounds[i] != other.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + self.limbs[i].enforce_equal(&other.limbs[i])?; + } + Ok(()) + } +} + +impl GR1CSVar for NonNativeUintVar { + type Value = BigInt; + + fn cs(&self) -> ConstraintSystemRef { + self.limbs.cs() + } + + fn value(&self) -> Result { + let mut r = BigInt::zero(); + + for limb in self.limbs.value()?.into_iter().rev() { + r <<= F::BITS_PER_LIMB; + r += if limb.into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO { + BigInt::from_biguint(Sign::Minus, (-limb).into()) + } else { + BigInt::from_biguint(Sign::Plus, limb.into()) + }; + } + + Ok(r) + } +} + +impl CondSelectGadget for NonNativeUintVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.limbs.len() != false_value.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if true_value.bounds.len() != false_value.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + let mut v = vec![]; + let mut bounds = vec![]; + for i in 0..true_value.limbs.len() { + if true_value.bounds[i] != false_value.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + v.push(cond.select(&true_value.limbs[i], &false_value.limbs[i])?); + bounds.push(true_value.bounds[i].clone()); + } + Ok(Self { + limbs: v, + bounds: bounds, + }) + } +} + +impl NonNativeUintVar { + fn constant(v: BigInt) -> Result { + Self::new_constant( + ConstraintSystemRef::None, + (v.clone(), Bound::new(v.clone(), v)), + ) + } + + fn ubound(&self) -> BigInt { + let mut r = BigInt::zero(); + + for i in self.bounds.iter().rev() { + r <<= F::BITS_PER_LIMB; + r += &i.ub; + } + + r + } + + fn lbound(&self) -> BigInt { + let mut r = BigInt::zero(); + + for i in self.bounds.iter().rev() { + r <<= F::BITS_PER_LIMB; + r += &i.lb; + } + + r + } +} + +impl NonNativeUintVar { + /// Enforce `self` to be less than `other`, where `self` and `other` should + /// be aligned. + /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 + pub fn enforce_lt(&self, other: &Self) -> Result<(), SynthesisError> { + let len = max(self.limbs.len(), other.limbs.len()); + let zero = FpVar::zero(); + + // Compute the difference between limbs of `other` and `self`. + // Denote a positive limb by `+`, a negative limb by `-`, a zero limb by + // `0`, and an unknown limb by `?`. + // Then, for `self < other`, `delta` should look like: + // ? ? ... ? ? + 0 0 ... 0 0 + let delta = (0..len) + .map(|i| { + let x = self.limbs.get(i).unwrap_or(&zero); + let y = other.limbs.get(i).unwrap_or(&zero); + y - x + }) + .collect::>(); + + // `helper` is a vector of booleans that indicates if the corresponding + // limb of `delta` is the first (searching from MSB) positive limb. + // For example, if `delta` is: + // - + ... + - + 0 0 ... 0 0 + // <---- search in this direction -------- + // Then `helper` should be: + // F F ... F F T F F ... F F + let helper = { + let cs = self.cs().or(other.cs()); + let mut helper = vec![false; len]; + for i in (0..len).rev() { + let delta = delta[i].value().unwrap_or_default().into_bigint(); + if !delta.is_zero() && delta < F::MODULUS_MINUS_ONE_DIV_TWO { + helper[i] = true; + break; + } + } + Vec::>::new_variable_with_inferred_mode(cs, || Ok(helper))? + }; + + // `p` is the first positive limb in `delta`. + let mut p = FpVar::::zero(); + // `r` is the sum of all bits in `helper`, which should be 1 when `self` + // is less than `other`, as there should be more than one positive limb + // in `delta`, and thus exactly one true bit in `helper`. + let mut r = FpVar::zero(); + for (b, d) in helper.into_iter().zip(delta) { + // Choose the limb `d` only if `b` is true. + p += b.select(&d, &FpVar::zero())?; + // Either `r` or `d` should be zero. + // Consider the same example as above: + // - + ... + - + 0 0 ... 0 0 + // F F ... F F T F F ... F F + // |-----------| + // `r = 0` in this range (before/when we meet the first positive limb) + // |---------| + // `d = 0` in this range (after we meet the first positive limb) + // This guarantees that for every bit after the true bit in `helper`, + // the corresponding limb in `delta` is zero. + (&r * &d).enforce_equal(&FpVar::zero())?; + // Add the current bit to `r`. + r += FpVar::from(b); + } + + // Ensure that `r` is exactly 1. This guarantees that there is exactly + // one true value in `helper`. + r.enforce_equal(&FpVar::one())?; + // Ensure that `p` is positive, i.e., + // `0 <= p - 1 < 2^bits_per_limb < F::MODULUS_MINUS_ONE_DIV_TWO`. + // This guarantees that the true value in `helper` corresponds to a + // positive limb in `delta`. + Self::enforce_bit_length(&(p - FpVar::one()), F::BITS_PER_LIMB)?; + + Ok(()) + } + + /// Enforce `self` to be equal to `other`, where `self` and `other` are not + /// necessarily aligned. + /// + /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L562-L798 + /// Similar implementations can also be found in https://github.com/alex-ozdemir/bellman-bignat/blob/0585b9d90154603a244cba0ac80b9aafe1d57470/src/mp/bignat.rs#L566-L661 + /// and https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/fields/emulated_fp/reduce.rs#L201-L323 + pub fn enforce_equal_unaligned(&self, other: &Self) -> Result<(), SynthesisError> { + let len = min(self.limbs.len(), other.limbs.len()); + + // Group the limbs of `self` and `other` so that each group nearly + // reaches the capacity `F::MODULUS_MINUS_ONE_DIV_TWO`. + // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` + // is the initial number of bits in a limb, just as what we do in grade + // school arithmetic, e.g., + // 5 9 + // x 7 3 + // ------------- + // 15 27 + // 35 63 + // ------------- <- When grouping 35, 15 + 63, and 27, we are computing + // 4 3 0 7 35 * 100 + (15 + 63) * 10 + 27 = 4307 + // Note that this is different from the concatenation `x_0 || x_1 ...`, + // since the bit-length of each limb is not necessarily the initial size + // `W`. + + let mut i = 0; + // `c` stores the current carry of `x_i - y_i` + let mut c = FpVar::::zero(); + while i < len { + let mut j = i; + // The current grouped limbs of `self` and `other`. + let mut p_limb = FpVar::zero(); + let mut q_limb = FpVar::zero(); + let mut p_bound = Bound::zero(); + let mut q_bound = Bound::zero(); + let mut step = 0; + let mut weight = F::one(); + while j < len { + match ( + self.bounds[j].shl(step).add(&p_bound).filter_safe::(), + other.bounds[j].shl(step).add(&q_bound).filter_safe::(), + ) { + (Some(new_p_bound), Some(new_q_bound)) => { + p_limb += &self.limbs[j] * weight; + q_limb += &other.limbs[j] * weight; + p_bound = new_p_bound; + q_bound = new_q_bound; + } + _ => break, + } + + j += 1; + step += F::BITS_PER_LIMB; + weight *= F::from(BigUint::one() << F::BITS_PER_LIMB); + } + // For each group, check the last `step_i` bits of `x_i` and `y_i` are + // equal. + // The intuition is to check `diff = x_i - y_i = 0 (mod 2^step_i)`. + // However, this is only true for `i = 0`, and we need to consider carry + // values `diff >> step_i` for `i > 0`. + // Therefore, we actually check `diff = x_i - y_i + c = 0 (mod 2^step_i)` + // and derive the next `c` by computing `diff >> step_i`. + // To enforce `diff = 0 (mod 2^step_i)`, we compute `diff / 2^step_i` + // and enforce it to be small (soundness holds because for `a` that does + // not divide `b`, `b / a` in the field will be very large). + c = (&p_limb - &q_limb + &c) * weight.inverse().unwrap(); + if j < len { + // Unlike the code mentioned above which add some offset to the + // diff `x_i - y_i + c` to make it always positive, we directly + // check if the absolute value of the diff is small. + Self::enforce_abs_bit_length( + &c, + (max( + min(&p_bound.lb, &q_bound.lb).bits(), + max(&p_bound.ub, &q_bound.ub).bits(), + ) as usize) + .checked_sub(step) + .unwrap_or_default(), + )?; + } else { + let remaining_limbs = &(if j < self.limbs.len() { self } else { other }).limbs[j..]; + let remaining_bounds = + &(if j < self.bounds.len() { self } else { other }).bounds[j..]; + if remaining_limbs.is_empty() { + c.enforce_equal(&FpVar::zero())?; + } else { + // If there is any remaining limb, the first one should be the + // final carry (which will be checked later), and the following + // ones should be zero. + + // Enforce the remaining limbs to be zero. + // Instead of doing that one by one, we check if their sum is + // zero using a single constraint. + // This is sound, as the upper bounds of the limbs and their sum + // are guaranteed to be less than `F::MODULUS_MINUS_ONE_DIV_TWO` + // (i.e., all of them are "non-negative"), implying that all + // limbs should be zero to make the sum zero. + remaining_limbs[1..] + .iter() + .sum::>() + .enforce_equal(&FpVar::zero())?; + Bound::add_many(remaining_bounds) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + // For the final carry, we need to ensure that it equals the + // remaining limb `rest`. + c.enforce_equal(&remaining_limbs[0])?; + }; + } + // Start the next group + i = j; + } + + Ok(()) + } +} + +impl NonNativeUintVar { + fn enforce_bit_length(x: &FpVar, length: usize) -> Result>, SynthesisError> { + let cs = x.cs(); + + let bits = &x.value().unwrap_or_default().into_bigint().to_bits_le()[..length]; + let bits = if cs.is_none() { + Vec::new_constant(cs, bits)? + } else { + Vec::new_witness(cs, || Ok(bits))? + }; + + Boolean::le_bits_to_fp(&bits)?.enforce_equal(x)?; + + Ok(bits) + } + + fn enforce_abs_bit_length( + x: &FpVar, + length: usize, + ) -> Result>, SynthesisError> { + let cs = x.cs(); + let mode = if cs.is_none() { + AllocationMode::Constant + } else { + AllocationMode::Witness + }; + + let is_neg = Boolean::new_variable( + cs.clone(), + || Ok(x.value().unwrap_or_default().into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO), + mode, + )?; + let bits = Vec::new_variable( + cs.clone(), + || { + Ok({ + let x = x.value().unwrap_or_default(); + let mut bits = if is_neg.value().unwrap_or_default() { + -x + } else { + x + } + .into_bigint() + .to_bits_le(); + bits.resize(length, false); + bits + }) + }, + mode, + )?; + + // Below is equivalent to but more efficient than + // `Boolean::le_bits_to_fp(&bits)?.enforce_equal(&is_neg.select(&x.negate()?, &x)?)?` + // Note that this enforces: + // 1. The claimed absolute value `is_neg.select(&x.negate()?, &x)?` has + // exactly `length` bits. + // 2. `is_neg` is indeed the sign of `x`, i.e., `is_neg = false` when + // `0 <= x < (|F| - 1) / 2`, and `is_neg = true` when + // `(|F| - 1) / 2 <= x < F`, thus the claimed absolute value is + // correct. + // If `is_neg` is incorrect, then: + // a. `0 <= x < (|F| - 1) / 2`, but `is_neg = true`, then + // `is_neg.select(&x.negate()?, &x)?` returns `|F| - x`, + // which is greater than `(|F| - 1) / 2` and cannot fit in + // `length` bits (given that `length` is small). + // b. `(|F| - 1) / 2 <= x < F`, but `is_neg = false`, then + // `is_neg.select(&x.negate()?, &x)?` returns `x`, which is + // greater than `(|F| - 1) / 2` and cannot fit in `length` + // bits. + FpVar::from(is_neg).mul_equals(&x.double()?, &(x - Boolean::le_bits_to_fp(&bits)?))?; + + Ok(bits) + } + + /// Compute `self + other`, without aligning the limbs. + pub fn add_no_align(&self, other: &Self) -> Result { + let mut z = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; + let mut bounds = vec![Bound::zero(); z.len()]; + for (i, v) in self.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&self.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + z[i] += v; + } + for (i, v) in other.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&other.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + z[i] += v; + } + Ok(Self { + limbs: z, + bounds: bounds, + }) + } + + pub fn sub_no_align(&self, other: &Self) -> Result { + let mut z = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; + let mut bounds = vec![Bound::zero(); z.len()]; + for (i, v) in self.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&self.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + z[i] += v; + } + for (i, v) in other.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .sub(&other.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + z[i] -= v; + } + Ok(Self { + limbs: z, + bounds: bounds, + }) + } + + /// Compute `self * other`, without aligning the limbs. + /// Implements the O(n) approach described in xJsnark, Section IV.B.1) + pub fn mul_no_align(&self, other: &Self) -> Result { + let len = self.limbs.len() + other.limbs.len() - 1; + if self.is_constant() || other.is_constant() { + // Use the naive approach for constant operands, which costs no + // constraints. + let bounds = (0..len) + .map(|i| { + let start = max(i + 1, other.bounds.len()) - other.bounds.len(); + let end = min(i + 1, self.bounds.len()); + Bound::add_many( + &(start..end) + .map(|j| self.bounds[j].mul(&other.bounds[i - j])) + .collect::>(), + ) + .filter_safe::() + }) + .collect::>>() + .ok_or(SynthesisError::Unsatisfiable)?; + + let z = (0..len) + .map(|i| { + let start = max(i + 1, other.limbs.len()) - other.limbs.len(); + let end = min(i + 1, self.limbs.len()); + (start..end) + .map(|j| &self.limbs[j] * &other.limbs[i - j]) + .sum() + }) + .collect(); + return Ok(Self { + limbs: z, + bounds: bounds, + }); + } + let cs = self.cs().or(other.cs()); + let mode = if cs.is_none() { + AllocationMode::Constant + } else { + AllocationMode::Witness + }; + + // Compute the result `z` outside the circuit and provide it as hints. + let (z, bounds) = { + let mut z = vec![F::zero(); len]; + let mut bounds = vec![Bound::zero(); len]; + for i in 0..self.limbs.len() { + for j in 0..other.limbs.len() { + z[i + j] += self.limbs[i].value().unwrap_or_default() + * other.limbs[j].value().unwrap_or_default(); + bounds[i + j] = bounds[i + j].add(&self.bounds[i].mul(&other.bounds[j])) + } + } + ( + Vec::new_variable(cs.clone(), || Ok(z), mode)?, + bounds + .into_iter() + .map(|b| b.filter_safe::()) + .collect::>() + .ok_or(SynthesisError::Unsatisfiable)?, + ) + }; + for c in 1..=len { + let c = F::from(c as u64); + let mut t = F::one(); + let mut c_powers = vec![]; + for _ in 0..len { + c_powers.push(t); + t *= c; + } + // `l = Σ self[i] c^i` + let l = self + .limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // `r = Σ other[i] c^i` + let r = other + .limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // `o = Σ z[i] c^i` + let o = z + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // Enforce `o = l * r` + l.mul_equals(&r, &o)?; + } + + Ok(Self { + limbs: z, + bounds: bounds, + }) + } + + /// Convert `Self` to an element in `M`, i.e., compute `Self % M::MODULUS`. + pub fn modulo(&self) -> Result { + let cs = self.cs(); + let m = BigInt::from_biguint(Sign::Plus, M::MODULUS.into()); + // Provide the quotient and remainder as hints + let q = Self::new_variable_with_inferred_mode(cs.clone(), || { + let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); + Ok((self.value()?.div_floor(&m), Bound::new(lb, ub))) + })?; + let r = Self::new_variable_with_inferred_mode(cs.clone(), || { + Ok((self.value()?.abs() % &m, Bound::new_ub(m.clone()))) + })?; + + let m = Self::constant(m)?; + + // Enforce `self = q * m + r` + q.mul_no_align(&m)? + .add_no_align(&r)? + .enforce_equal_unaligned(self)?; + // Enforce `r < m` (and `r >= 0` already holds) + r.enforce_lt(&m)?; + + Ok(r) + } + + /// Enforce that `self` is congruent to `other` modulo `M::MODULUS`. + pub fn enforce_congruent(&self, other: &Self) -> Result<(), SynthesisError> { + let cs = self.cs(); + let m = BigInt::from_biguint(Sign::Plus, M::MODULUS.into()); + // Provide the quotient as hint + let q = Self::new_variable_with_inferred_mode(cs.clone(), || { + let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); + Ok((self.value()?.div_floor(&m), Bound::new(lb, ub))) + })?; + + let m = Self::constant(m)?; + + // Enforce `self - other = q * m` + self.sub_no_align(other)? + .enforce_equal_unaligned(&q.mul_no_align(&m)?) + } +} + +impl EquivalenceGadget for NonNativeUintVar { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_congruent::(other) + } +} + +impl FromBitsGadget for NonNativeUintVar { + fn from_bits_le(bits: &[Boolean]) -> Result { + Ok(Self { + limbs: bits + .as_ref() + .chunks(F::BITS_PER_LIMB) + .map(Boolean::le_bits_to_fp) + .collect::>()?, + bounds: bits + .as_ref() + .chunks(F::BITS_PER_LIMB) + .map(|i| Bound::new_n_bits(i.len())) + .collect(), + }) + } +} + +impl ToBitsGadget for NonNativeUintVar { + fn to_bits_le(&self) -> Result>, SynthesisError> { + Ok(self + .limbs + .iter() + .map(|limb| limb.to_n_bits_le(F::BITS_PER_LIMB)) + .collect::, _>>()? + .concat()) + } +} + +impl AbsorbableGadget> for NonNativeUintVar { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; + + self.to_bits_le()? + .chunks(bits_per_limb) + .try_for_each(|i| Ok(dest.push(Boolean::le_bits_to_fp(i)?))) + } +} + +impl VectorGadget> for [NonNativeUintVar] { + fn add(&self, other: &Self) -> Result>, SynthesisError> { + self.iter() + .zip(other.iter()) + .map(|(x, y)| x.add_no_align(y)) + .collect() + } + + fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { + self.iter() + .zip(other.iter()) + .map(|(x, y)| x.mul_no_align(y)) + .collect() + } + + fn scale( + &self, + other: &NonNativeUintVar, + ) -> Result>, SynthesisError> { + self.iter().map(|x| x.mul_no_align(other)).collect() + } +} + +impl MatrixGadget> for SparseMatrixVar> { + fn mul_vector( + &self, + v: &impl Index>, + ) -> Result>, SynthesisError> { + self.0 + .iter() + .map(|row| { + let len = row + .iter() + .map(|(value, col_i)| value.limbs.len() + v[*col_i].limbs.len() - 1) + .max() + .unwrap_or(0); + // This is a combination of `mul_no_align` and `add_no_align` + // that results in more flattened `LinearCombination`s. + // Consequently, `ConstraintSystem::inline_all_lcs` costs less + // time, thus making trusted setup and proof generation faster. + let bounds = (0..len) + .map(|i| { + Bound::add_many( + &row.iter() + .flat_map(|(value, col_i)| { + let start = + max(i + 1, v[*col_i].bounds.len()) - v[*col_i].bounds.len(); + let end = min(i + 1, value.bounds.len()); + (start..end) + .map(|j| value.bounds[j].mul(&v[*col_i].bounds[i - j])) + }) + .collect::>(), + ) + .filter_safe::() + }) + .collect::>>() + .ok_or(SynthesisError::Unsatisfiable)?; + let v = (0..len) + .map(|i| { + row.iter() + .flat_map(|(value, col_i)| { + let start = + max(i + 1, v[*col_i].limbs.len()) - v[*col_i].limbs.len(); + let end = min(i + 1, value.limbs.len()); + (start..end).map(|j| &value.limbs[j] * &v[*col_i].limbs[i - j]) + }) + .sum() + }) + .collect(); + Ok(NonNativeUintVar { + limbs: v, + bounds: bounds, + }) + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use std::error::Error; + + use ark_ff::Field; + use ark_pallas::{Fq, Fr}; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{test_rng, UniformRand}; + use num_bigint::RandBigInt; + + use super::*; + + #[test] + fn test_mul_biguint() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let size = 256; + + let rng = &mut test_rng(); + let a = rng.gen_biguint(size as u64); + let b = rng.gen_biguint(size as u64); + let ab = &a * &b; + let aab = &a * &ab; + let abb = &ab * &b; + + let a_var = + NonNativeUintVar::new_witness(cs.clone(), || Ok((a.into(), Bound::new_n_bits(size))))?; + let b_var = + NonNativeUintVar::new_witness(cs.clone(), || Ok((b.into(), Bound::new_n_bits(size))))?; + let ab_var = NonNativeUintVar::new_witness(cs.clone(), || { + Ok((ab.into(), Bound::new_n_bits(size * 2))) + })?; + let aab_var = NonNativeUintVar::new_witness(cs.clone(), || { + Ok((aab.into(), Bound::new_n_bits(size * 3))) + })?; + let abb_var = NonNativeUintVar::new_witness(cs.clone(), || { + Ok((abb.into(), Bound::new_n_bits(size * 3))) + })?; + + a_var + .mul_no_align(&b_var)? + .enforce_equal_unaligned(&ab_var)?; + a_var + .mul_no_align(&ab_var)? + .enforce_equal_unaligned(&aab_var)?; + ab_var + .mul_no_align(&b_var)? + .enforce_equal_unaligned(&abb_var)?; + + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_mul_fq() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let rng = &mut test_rng(); + let a = Fq::rand(rng); + let b = Fq::rand(rng); + let ab = a * b; + let aab = a * ab; + let abb = ab * b; + + let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; + let b_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(b))?; + let ab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(ab))?; + let aab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(aab))?; + let abb_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(abb))?; + + a_var + .mul_no_align(&b_var)? + .enforce_congruent::(&ab_var)?; + a_var + .mul_no_align(&ab_var)? + .enforce_congruent::(&aab_var)?; + ab_var + .mul_no_align(&b_var)? + .enforce_congruent::(&abb_var)?; + + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_pow() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let rng = &mut test_rng(); + + let a = Fq::rand(rng); + + let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; + + let mut r_var = a_var.clone(); + for _ in 0..16 { + r_var = r_var.mul_no_align(&r_var)?.modulo::()?; + } + r_var = r_var.mul_no_align(&a_var)?.modulo::()?; + assert_eq!( + BigInt::from_biguint(Sign::Plus, a.pow([65537u64]).into()), + r_var.value()? + ); + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_vec_vec_mul() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let len = 1000; + + let rng = &mut test_rng(); + let a = (0..len).map(|_| Fq::rand(rng)).collect::>(); + let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); + let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); + + let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; + let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; + let c_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(c))?; + + let mut r_var = NonNativeUintVar::constant(BigUint::zero().into())?; + for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { + r_var = r_var.add_no_align(&a.mul_no_align(&b)?)?; + } + r_var.enforce_congruent::(&c_var)?; + println!("{}", cs.num_constraints()); + + assert!(cs.is_satisfied()?); + Ok(()) + } +} diff --git a/crates/primitives/src/algebra/field/nonnative2.rs b/crates/primitives/src/algebra/field/nonnative2.rs new file mode 100644 index 000000000..f16daf950 --- /dev/null +++ b/crates/primitives/src/algebra/field/nonnative2.rs @@ -0,0 +1,1159 @@ +use ark_ff::{BigInteger, One, PrimeField, Zero}; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + convert::ToBitsGadget, + fields::{fp::FpVar, FieldVar}, + prelude::EqGadget, + select::CondSelectGadget, + GR1CSVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + cmp::{max, min}, + marker::PhantomData, + ops::Index, +}; +use num_bigint::{BigInt, BigUint, Sign}; +use num_integer::Integer; +use num_traits::Signed; + +use crate::algebra::ops::bits::{FromBitsGadget, ToBitsGadgetExt}; +use crate::{ + algebra::{ + field::SonobeField, + ops::{ + eq::EquivalenceGadget, + matrix::{MatrixGadget, SparseMatrixVar}, + vector::VectorGadget, + }, + }, + transcripts::AbsorbableGadget, +}; + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Bound(pub BigInt, pub BigInt); + +impl Bound { + pub fn zero() -> Self { + Self::default() + } +} + +impl Bound { + pub fn add(&self, other: &Self) -> Self { + Self(&self.0 + &other.0, &self.1 + &other.1) + } + + pub fn sub(&self, other: &Self) -> Self { + Self(&self.0 - &other.1, &self.1 - &other.0) + } + + pub fn add_many(limbs: &[Self]) -> Self { + Self( + limbs.iter().map(|l| &l.0).sum(), + limbs.iter().map(|l| &l.1).sum(), + ) + } + + pub fn mul(&self, other: &Self) -> Self { + let ll = &self.0 * &other.0; + let lu = &self.0 * &other.1; + let ul = &self.1 * &other.0; + let uu = &self.1 * &other.1; + + Self( + min(min(&ll, &lu), min(&ul, &uu)).clone(), + max(max(&ll, &lu), max(&ul, &uu)).clone(), + ) + } + + pub fn shl(&self, shift: usize) -> Self { + Self(&self.0 << shift, &self.1 << shift) + } + + pub fn filter_safe(self) -> Option { + let limit = BigInt::from_biguint(Sign::Plus, F::MODULUS_MINUS_ONE_DIV_TWO.into()); + (self.0 >= -&limit && self.1 <= limit).then_some(self) + } +} + +#[derive(Clone)] +pub struct IntVarInner { + _cfg: PhantomData, + limbs: Vec>, + bounds: Vec, +} + +pub type BigIntVar = IntVarInner; +pub type NonNativeFieldVar = + IntVarInner; + +impl GR1CSVar for IntVarInner { + type Value = BigInt; + + fn cs(&self) -> ConstraintSystemRef { + self.limbs.cs() + } + + fn value(&self) -> Result { + let mut r = BigInt::zero(); + + for limb in self.limbs.value()?.into_iter().rev() { + r <<= F::BITS_PER_LIMB; + r += if limb.into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO { + BigInt::from_biguint(Sign::Minus, (-limb).into()) + } else { + BigInt::from_biguint(Sign::Plus, limb.into()) + }; + } + + Ok(r) + } +} + +impl IntVarInner { + fn new(limbs: Vec>, bounds: Vec) -> Self { + Self { + _cfg: PhantomData, + limbs, + bounds, + } + } + + fn ubound(&self) -> BigInt { + let mut r = BigInt::zero(); + + for i in self.bounds.iter().rev() { + r <<= F::BITS_PER_LIMB; + r += &i.1; + } + + r + } + + fn lbound(&self) -> BigInt { + let mut r = BigInt::zero(); + + for i in self.bounds.iter().rev() { + r <<= F::BITS_PER_LIMB; + r += &i.0; + } + + r + } +} + +impl IntVarInner { + /// Enforce `self` to be less than `other`, where `self` and `other` should + /// be aligned. + /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 + pub fn enforce_lt(&self, other: &Self) -> Result<(), SynthesisError> { + let len = max(self.limbs.len(), other.limbs.len()); + let zero = FpVar::zero(); + + // Compute the difference between limbs of `other` and `self`. + // Denote a positive limb by `+`, a negative limb by `-`, a zero limb by + // `0`, and an unknown limb by `?`. + // Then, for `self < other`, `delta` should look like: + // ? ? ... ? ? + 0 0 ... 0 0 + let delta = (0..len) + .map(|i| { + let x = self.limbs.get(i).unwrap_or(&zero); + let y = other.limbs.get(i).unwrap_or(&zero); + y - x + }) + .collect::>(); + + // `helper` is a vector of booleans that indicates if the corresponding + // limb of `delta` is the first (searching from MSB) positive limb. + // For example, if `delta` is: + // - + ... + - + 0 0 ... 0 0 + // <---- search in this direction -------- + // Then `helper` should be: + // F F ... F F T F F ... F F + let helper = { + let cs = self.limbs.cs().or(other.limbs.cs()); + let mut helper = vec![false; len]; + for i in (0..len).rev() { + let delta = delta[i].value().unwrap_or_default().into_bigint(); + if !delta.is_zero() && delta < F::MODULUS_MINUS_ONE_DIV_TWO { + helper[i] = true; + break; + } + } + Vec::>::new_variable_with_inferred_mode(cs, || Ok(helper))? + }; + + // `p` is the first positive limb in `delta`. + let mut p = FpVar::::zero(); + // `r` is the sum of all bits in `helper`, which should be 1 when `self` + // is less than `other`, as there should be more than one positive limb + // in `delta`, and thus exactly one true bit in `helper`. + let mut r = FpVar::zero(); + for (b, d) in helper.into_iter().zip(delta) { + // Choose the limb `d` only if `b` is true. + p += b.select(&d, &FpVar::zero())?; + // Either `r` or `d` should be zero. + // Consider the same example as above: + // - + ... + - + 0 0 ... 0 0 + // F F ... F F T F F ... F F + // |-----------| + // `r = 0` in this range (before/when we meet the first positive limb) + // |---------| + // `d = 0` in this range (after we meet the first positive limb) + // This guarantees that for every bit after the true bit in `helper`, + // the corresponding limb in `delta` is zero. + (&r * &d).enforce_equal(&FpVar::zero())?; + // Add the current bit to `r`. + r += FpVar::from(b); + } + + // Ensure that `r` is exactly 1. This guarantees that there is exactly + // one true value in `helper`. + r.enforce_equal(&FpVar::one())?; + // Ensure that `p` is positive, i.e., + // `0 <= p - 1 < 2^bits_per_limb < F::MODULUS_MINUS_ONE_DIV_TWO`. + // This guarantees that the true value in `helper` corresponds to a + // positive limb in `delta`. + (p - FpVar::one()).enforce_bit_length(F::BITS_PER_LIMB)?; + + Ok(()) + } +} + +impl From> for IntVarInner { + fn from(v: IntVarInner) -> Self { + Self::new(v.limbs, v.bounds) + } +} + +impl IntVarInner { + /// Compute `self + other`, without aligning the limbs. + pub fn add_unaligned( + &self, + other: &IntVarInner, + ) -> Result, SynthesisError> { + let mut limbs = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; + let mut bounds = vec![Bound::zero(); limbs.len()]; + for (i, v) in self.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&self.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] += v; + } + for (i, v) in other.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&other.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] += v; + } + Ok(IntVarInner::new(limbs, bounds)) + } + + pub fn sub_unaligned( + &self, + other: &IntVarInner, + ) -> Result, SynthesisError> { + let mut limbs = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; + let mut bounds = vec![Bound::zero(); limbs.len()]; + for (i, v) in self.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .add(&self.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] += v; + } + for (i, v) in other.limbs.iter().enumerate() { + bounds[i] = bounds[i] + .sub(&other.bounds[i]) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + limbs[i] -= v; + } + Ok(IntVarInner::new(limbs, bounds)) + } + + /// Compute `self * other`, without aligning the limbs. + /// Implements the O(n) approach described in xJsnark, Section IV.B.1) + pub fn mul_unaligned( + &self, + other: &IntVarInner, + ) -> Result, SynthesisError> { + let len = self.limbs.len() + other.limbs.len() - 1; + if self.limbs.is_constant() || other.limbs.is_constant() { + // Use the naive approach for constant operands, which costs no + // constraints. + let bounds = (0..len) + .map(|i| { + let start = max(i + 1, other.bounds.len()) - other.bounds.len(); + let end = min(i + 1, self.bounds.len()); + Bound::add_many( + &(start..end) + .map(|j| self.bounds[j].mul(&other.bounds[i - j])) + .collect::>(), + ) + .filter_safe::() + }) + .collect::>>() + .ok_or(SynthesisError::Unsatisfiable)?; + + let limbs = (0..len) + .map(|i| { + let start = max(i + 1, other.limbs.len()) - other.limbs.len(); + let end = min(i + 1, self.limbs.len()); + (start..end) + .map(|j| &self.limbs[j] * &other.limbs[i - j]) + .sum() + }) + .collect(); + return Ok(IntVarInner::new(limbs, bounds)); + } + // Compute the product `limbs` outside the circuit and provide it as + // hints. + let (limbs, bounds) = { + let cs = self.limbs.cs().or(other.limbs.cs()); + let mut limbs = vec![F::zero(); len]; + let mut bounds = vec![Bound::zero(); len]; + for i in 0..self.limbs.len() { + for j in 0..other.limbs.len() { + limbs[i + j] += self.limbs[i].value().unwrap_or_default() + * other.limbs[j].value().unwrap_or_default(); + bounds[i + j] = bounds[i + j].add(&self.bounds[i].mul(&other.bounds[j])) + } + } + ( + Vec::new_variable_with_inferred_mode(cs, || Ok(limbs))?, + bounds + .into_iter() + .map(|b| b.filter_safe::()) + .collect::>() + .ok_or(SynthesisError::Unsatisfiable)?, + ) + }; + for c in 1..=len { + let c = F::from(c as u64); + let mut t = F::one(); + let mut c_powers = vec![]; + for _ in 0..len { + c_powers.push(t); + t *= c; + } + // `l = Σ self[i] c^i` + let l = self + .limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // `r = Σ other[i] c^i` + let r = other + .limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // `o = Σ z[i] c^i` + let o = limbs + .iter() + .zip(&c_powers) + .map(|(v, t)| v * *t) + .sum::>(); + // Enforce `o = l * r` + l.mul_equals(&r, &o)?; + } + + Ok(IntVarInner::new(limbs, bounds)) + } + + /// Enforce `self` to be equal to `other`, where `self` and `other` are not + /// necessarily aligned. + /// + /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L562-L798 + /// Similar implementations can also be found in https://github.com/alex-ozdemir/bellman-bignat/blob/0585b9d90154603a244cba0ac80b9aafe1d57470/src/mp/bignat.rs#L566-L661 + /// and https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/fields/emulated_fp/reduce.rs#L201-L323 + pub fn enforce_equal_unaligned( + &self, + other: &IntVarInner, + ) -> Result<(), SynthesisError> { + let len = min(self.limbs.len(), other.limbs.len()); + + // Group the limbs of `self` and `other` so that each group nearly + // reaches the capacity `F::MODULUS_MINUS_ONE_DIV_TWO`. + // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` + // is the initial number of bits in a limb, just as what we do in grade + // school arithmetic, e.g., + // 5 9 + // x 7 3 + // ------------- + // 15 27 + // 35 63 + // ------------- <- When grouping 35, 15 + 63, and 27, we are computing + // 4 3 0 7 35 * 100 + (15 + 63) * 10 + 27 = 4307 + // Note that this is different from the concatenation `x_0 || x_1 ...`, + // since the bit-length of each limb is not necessarily the initial size + // `W`. + + let mut i = 0; + let mut diff = FpVar::zero(); + let mut x_bound = Bound::zero(); + let mut y_bound = Bound::zero(); + let mut step = 0; + let inv = F::from(BigUint::one() << F::BITS_PER_LIMB) + .inverse() + .unwrap(); + + while i < len { + if let (Some(new_x_bound), Some(new_y_bound)) = ( + self.bounds[i].shl(step).add(&x_bound).filter_safe::(), + other.bounds[i].shl(step).add(&y_bound).filter_safe::(), + ) { + diff = (diff + &self.limbs[i] - &other.limbs[i]) * inv; + x_bound = new_x_bound; + y_bound = new_y_bound; + + i += 1; + step += F::BITS_PER_LIMB; + continue; + } + // For each group, check the last `step_i` bits of `x_i` and `y_i` are + // equal. + // The intuition is to check `diff = x_i - y_i = 0 (mod 2^step_i)`. + // However, this is only true for `i = 0`, and we need to consider carry + // values `diff >> step_i` for `i > 0`. + // Therefore, we actually check `diff = x_i - y_i + c = 0 (mod 2^step_i)` + // and derive the next `c` by computing `diff >> step_i`. + // To enforce `diff = 0 (mod 2^step_i)`, we compute `diff / 2^step_i` + // and enforce it to be small (soundness holds because for `a` that does + // not divide `b`, `b / a` in the field will be very large). + let bits = (max( + min(&x_bound.0, &y_bound.0).bits(), + max(&x_bound.1, &y_bound.1).bits(), + ) as usize) + .checked_sub(step) + .unwrap_or_default(); + + (&diff + F::from(BigUint::one() << bits)).enforce_bit_length(bits + 1)?; + + x_bound = Bound::zero(); + y_bound = Bound::zero(); + step = 0; + } + + let remaining_limbs = if i < self.limbs.len() { + &self.limbs[i..] + } else { + &other.limbs[i..] + }; + let remaining_bounds = if i < self.bounds.len() { + &self.bounds[i..] + } else { + &other.bounds[i..] + }; + if remaining_limbs.is_empty() { + diff.enforce_equal(&FpVar::zero())?; + } else { + // If there is any remaining limb, the first one should be the + // final carry (which will be checked later), and the following + // ones should be zero. + + // Enforce the remaining limbs to be zero. + // Instead of doing that one by one, we check if their sum is + // zero using a single constraint. + // This is sound, as the upper bounds of the limbs and their sum + // are guaranteed to be less than `F::MODULUS_MINUS_ONE_DIV_TWO` + // (i.e., all of them are "non-negative"), implying that all + // limbs should be zero to make the sum zero. + remaining_limbs[1..] + .iter() + .sum::>() + .enforce_equal(&FpVar::zero())?; + Bound::add_many(remaining_bounds) + .filter_safe::() + .ok_or(SynthesisError::Unsatisfiable)?; + // For the final carry, we need to ensure that it equals the + // remaining limb `rest`. + diff.enforce_equal(&remaining_limbs[0])?; + } + + Ok(()) + } +} + +impl + IntVarInner +{ + /// Convert `Self` to an element in `M`, i.e., compute `Self % M::MODULUS`. + pub fn modulo(&self) -> Result, SynthesisError> { + let cs = self.cs(); + let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); + // Provide the quotient and remainder as hints + let q = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { + let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); + Ok((self.value()?.div_floor(&m), Bound(lb, ub))) + })?; + let r = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { + Ok((self.value()?.abs() % &m, Bound(Zero::zero(), m.clone()))) + })?; + + let m = IntVarInner::constant(m); + + // Enforce `self = q * m + r` + q.mul_unaligned(&m)? + .add_unaligned(&r)? + .enforce_equal_unaligned(self)?; + // Enforce `r < m` (and `r >= 0` already holds) + r.enforce_lt(&m)?; + + Ok(r) + } + + /// Enforce that `self` is congruent to `other` modulo `M::MODULUS`. + pub fn enforce_congruent( + &self, + other: &IntVarInner, + ) -> Result<(), SynthesisError> { + let cs = self.cs(); + let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); + // Provide the quotient as hint + let q = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { + let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); + Ok((self.value()?.div_floor(&m), Bound(lb, ub))) + })?; + + let m = IntVarInner::constant(m); + + // Enforce `self - other = q * m` + self.sub_unaligned(other)? + .enforce_equal_unaligned(&q.mul_unaligned(&m)?) + } +} + +impl TryFrom> + for IntVarInner +{ + type Error = SynthesisError; + + fn try_from(v: IntVarInner) -> Result { + v.modulo() + } +} + +// pub enum NonNativeUintVar { +// Aligned(UintVarInner), +// Unaligned(UintVarInner), +// } + +impl FromBitsGadget for IntVarInner { + fn from_bits_le(bits: &[Boolean]) -> Result { + Ok(Self::new( + bits.chunks(F::BITS_PER_LIMB) + .map(Boolean::le_bits_to_fp) + .collect::>()?, + bits.chunks(F::BITS_PER_LIMB) + .map(|i| Bound(BigInt::zero(), (BigInt::one() << i.len()) - BigInt::one())) + .collect(), + )) + } +} + +impl ToBitsGadget for IntVarInner { + fn to_bits_le(&self) -> Result>, SynthesisError> { + for bound in &self.bounds { + assert!(bound.0 >= BigInt::zero()); + } + Ok(self + .limbs + .iter() + .zip(&self.bounds) + .map(|(limb, bound)| limb.to_n_bits_le(bound.1.bits() as usize)) + .collect::, _>>()? + .concat()) + } +} + +impl AbsorbableGadget> for IntVarInner { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; + + self.to_bits_le()? + .chunks(bits_per_limb) + .try_for_each(|i| Ok(dest.push(Boolean::le_bits_to_fp(i)?))) + } +} + + +impl VectorGadget> for [IntVarInner] { + fn add(&self, other: &Self) -> Result>, SynthesisError> { + self.iter() + .zip(other.iter()) + .map(|(x, y)| x.add_unaligned(y)) + .collect() + } + + fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { + self.iter() + .zip(other.iter()) + .map(|(x, y)| x.mul_unaligned(y)) + .collect() + } + + fn scale( + &self, + other: &IntVarInner, + ) -> Result>, SynthesisError> { + self.iter().map(|x| x.mul_unaligned(other)).collect() + } +} + +impl MatrixGadget> for SparseMatrixVar> { + fn mul_vector( + &self, + v: &impl Index>, + ) -> Result>, SynthesisError> { + self.0 + .iter() + .map(|row| { + let len = row + .iter() + .map(|(value, col_i)| value.limbs.len() + v[*col_i].limbs.len() - 1) + .max() + .unwrap_or(0); + // This is a combination of `mul_no_align` and `add_no_align` + // that results in more flattened `LinearCombination`s. + // Consequently, `ConstraintSystem::inline_all_lcs` costs less + // time, thus making trusted setup and proof generation faster. + let bounds = (0..len) + .map(|i| { + Bound::add_many( + &row.iter() + .flat_map(|(value, col_i)| { + let start = + max(i + 1, v[*col_i].bounds.len()) - v[*col_i].bounds.len(); + let end = min(i + 1, value.bounds.len()); + (start..end) + .map(|j| value.bounds[j].mul(&v[*col_i].bounds[i - j])) + }) + .collect::>(), + ) + .filter_safe::() + }) + .collect::>>() + .ok_or(SynthesisError::Unsatisfiable)?; + let limbs = (0..len) + .map(|i| { + row.iter() + .flat_map(|(value, col_i)| { + let start = + max(i + 1, v[*col_i].limbs.len()) - v[*col_i].limbs.len(); + let end = min(i + 1, value.limbs.len()); + (start..end).map(|j| &value.limbs[j] * &v[*col_i].limbs[i - j]) + }) + .sum() + }) + .collect(); + Ok(IntVarInner::new(limbs, bounds)) + }) + .collect() + } +} + +impl AllocVar<(BigInt, Bound), F> for IntVarInner { + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let (x, Bound(lb, ub)) = v.borrow(); + + if x < lb || x > ub { + return Err(SynthesisError::Unsatisfiable); + } + + let len = max(lb.bits(), ub.bits()) as usize; + + let x_is_neg = x.is_negative(); + let mut x_bits = x + .magnitude() + .to_radix_le(2) + .into_iter() + .map(|i| i == 1) + .collect::>(); + x_bits.resize(len, false); + + let x_is_neg = if !lb.is_negative() { + Boolean::FALSE + } else if !ub.is_positive() { + Boolean::TRUE + } else { + Boolean::new_variable(cs.clone(), || Ok(x_is_neg), mode)? + }; + let x_bits = Vec::new_variable(cs, || Ok(x_bits), mode)?; + + let (n_full_limbs, n_remaining_bits) = len.div_rem(&F::BITS_PER_LIMB); + + let limbs = x_bits + .chunks(F::BITS_PER_LIMB) + .map(|chunk| { + let limb_abs = Boolean::le_bits_to_fp(chunk)?; + x_is_neg.select(&limb_abs.negate()?, &limb_abs) + }) + .collect::>()?; + + let mut bounds = vec![ + Bound( + if lb.is_negative() { + BigInt::one() - (BigInt::one() << F::BITS_PER_LIMB) + } else { + BigInt::zero() + }, + if ub.is_positive() { + (BigInt::one() << F::BITS_PER_LIMB) - BigInt::one() + } else { + BigInt::zero() + }, + ); + n_full_limbs + ]; + + if !n_remaining_bits.is_zero() { + let d = BigInt::one() << (len - n_remaining_bits); + bounds.push(Bound(lb.div_floor(&d), ub.div_ceil(&d))); + } + + let var = Self::new(limbs, bounds); + + // At this point, we are confident that: + // * If `lb >= 0`, then `0 <= var <= 2^len - 1`. + // * If `ub <= 0`, then `-2^len + 1 <= var <= 0`. + // * Otherwise, `-2^len + 1 <= var <= 2^len - 1`. + // + // However, for soundness, we need to enforce `lb <= var <= ub`, which + // is already guaranteed only if: + // * `lb = 0` and `ub = 2^len - 1` + // * `lb = -2^len + 1` and `ub = 0` + // * `lb = -2^len + 1` and `ub = 2^len - 1` + // + // For other cases, we additionally check: + // * `var <= ub` + // * `var >= lb` + if lb.is_zero() && ub + BigInt::one() == BigInt::one() << len { + } else if BigInt::one() - lb == BigInt::one() << len && ub.is_zero() { + } else if BigInt::one() - lb == BigInt::one() << len + && ub + BigInt::one() == BigInt::one() << len + { + } else { + var.enforce_lt(&Self::constant(ub + BigInt::one()))?; + Self::constant(lb - BigInt::one()).enforce_lt(&var)?; + } + + Ok(var) + } + + fn new_constant( + _cs: impl Into>, + t: impl Borrow<(BigInt, Bound)>, + ) -> Result { + let (x, Bound(lb, ub)) = t.borrow(); + + if x < lb || x > ub { + return Err(SynthesisError::Unsatisfiable); + } + + // Ignore `lb` and `ub` from now on, as a constant `x` will be bounded + // by itself. + let bits = x + .magnitude() + .to_radix_le(2) + .into_iter() + .map(|i| i == 1) + .collect::>(); + + let (limbs, bounds) = bits + .chunks(F::BITS_PER_LIMB) + .map(F::BigInt::from_bits_le) + .map(|v| { + let v_field = if x.is_negative() { + -F::from(v) + } else { + F::from(v) + }; + let v_bigint = BigInt::from_biguint(x.sign(), v.into()); + (FpVar::constant(v_field), Bound(v_bigint.clone(), v_bigint)) + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + + Ok(Self::new(limbs, bounds)) + } +} + +impl AllocVar for IntVarInner { + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + Self::new_variable( + cs, + || { + f().map(|v| { + ( + BigInt::from_biguint(Sign::Plus, (*v.borrow()).into()), + Bound(Zero::zero(), G::MODULUS.into().into()), + ) + }) + }, + mode, + ) + } +} + +impl IntVarInner { + fn constant(x: BigInt) -> Self { + Self::new_constant(ConstraintSystemRef::None, (x.clone(), Bound(x.clone(), x))).unwrap() + } +} + +macro_rules! impl_binary_op { + ( + $trait: ident, + $fn: ident, + |$lhs_i:tt : &$lhs:ty, $rhs_i:tt : &$rhs:ty| -> $out:ty $body:block, + ($($params:tt)+), + ) => { + impl<$($params)+> core::ops::$trait<&$rhs> for &$lhs + { + type Output = $out; + + fn $fn(self, other: &$rhs) -> Self::Output { + let $lhs_i = self; + let $rhs_i = other; + $body + } + } + + impl<$($params)+> core::ops::$trait<$rhs> for &$lhs + { + type Output = $out; + + fn $fn(self, other: $rhs) -> Self::Output { + core::ops::$trait::$fn(self, &other) + } + } + + impl<$($params)+> core::ops::$trait<&$rhs> for $lhs + { + type Output = $out; + + fn $fn(self, other: &$rhs) -> Self::Output { + core::ops::$trait::$fn(&self, other) + } + } + + impl<$($params)+> core::ops::$trait<$rhs> for $lhs + { + type Output = $out; + + fn $fn(self, other: $rhs) -> Self::Output { + core::ops::$trait::$fn(&self, &other) + } + } + } +} + +macro_rules! impl_assignment_op { + ( + $assign_trait: ident, + $assign_fn: ident, + |$lhs_i:tt : &mut $lhs:ty, $rhs_i:tt : &$rhs:ty| $body:block, + ($($params:tt)+), + ) => { + impl<$($params)+> core::ops::$assign_trait<$rhs> for $lhs + { + fn $assign_fn(&mut self, other: $rhs) { + core::ops::$assign_trait::$assign_fn(self, &other) + } + } + + impl<$($params)+> core::ops::$assign_trait<&$rhs> for $lhs + { + fn $assign_fn(&mut self, other: &$rhs) { + let $lhs_i = self; + let $rhs_i = other; + $body + } + } + } +} + +impl_binary_op!( + Add, + add, + |a: &IntVarInner, b: &IntVarInner| -> IntVarInner { + a.add_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const LHS_ALIGNED: bool, const RHS_ALIGNED: bool), +); + +impl_assignment_op!( + AddAssign, + add_assign, + |a: &mut IntVarInner, b: &IntVarInner| { + *a = a.add_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const ALIGNED: bool), +); + +impl_binary_op!( + Sub, + sub, + |a: &IntVarInner, b: &IntVarInner| -> IntVarInner { + a.sub_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const SELF_ALIGNED: bool, const OTHER_ALIGNED: bool), +); + +impl_assignment_op!( + SubAssign, + sub_assign, + |a: &mut IntVarInner, b: &IntVarInner| { + *a = a.sub_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const OTHER_ALIGNED: bool), +); + +impl_binary_op!( + Mul, + mul, + |a: &IntVarInner, b: &IntVarInner| -> IntVarInner { + a.mul_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const SELF_ALIGNED: bool, const OTHER_ALIGNED: bool), +); + +impl_assignment_op!( + MulAssign, + mul_assign, + |a: &mut IntVarInner, b: &IntVarInner| { + *a = a.mul_unaligned(b).unwrap() + }, + (F: SonobeField, Cfg, const OTHER_ALIGNED: bool), +); + +#[cfg(test)] +mod tests { + use std::error::Error; + + use ark_ff::Field; + use ark_pallas::{Fq, Fr}; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{test_rng, UniformRand}; + use num_bigint::RandBigInt; + + use super::*; + + #[test] + fn test_alloc() -> Result<(), Box> { + let rng = &mut test_rng(); + + let size = 1024; + let mut lbs = vec![BigInt::zero()]; + let mut ubs: Vec = vec![(BigInt::one() << size) - BigInt::one()]; + lbs.push(-ubs[0].clone()); + ubs.push(BigInt::zero()); + lbs.push(-ubs[0].clone()); + ubs.push(ubs[0].clone()); + lbs.push(rng.gen_bigint_range(&-&ubs[0], &BigInt::zero())); + ubs.push(BigInt::zero()); + lbs.push(BigInt::zero()); + ubs.push(rng.gen_bigint_range(&BigInt::zero(), &ubs[0])); + lbs.push(rng.gen_bigint_range(&-&ubs[0], &BigInt::zero())); + ubs.push(rng.gen_bigint_range(&BigInt::zero(), &ubs[0])); + lbs.push(rng.gen_bigint_range(&-&ubs[0], &BigInt::zero())); + ubs.push(rng.gen_bigint_range(lbs.last().unwrap(), &BigInt::zero())); + lbs.push(rng.gen_bigint_range(&BigInt::zero(), &ubs[0])); + ubs.push(rng.gen_bigint_range(lbs.last().unwrap(), &ubs[0])); + + for (lb, ub) in lbs.into_iter().zip(ubs.into_iter()) { + let mut v = vec![ + lb.clone(), + ub.clone(), + &lb + BigInt::one(), + &ub - BigInt::one(), + ]; + if BigInt::zero() >= lb && BigInt::zero() <= ub { + v.push(BigInt::zero()); + } + for _ in 0..10 { + v.push(rng.gen_bigint_range(&lb, &ub)); + } + for a in v { + let cs = ConstraintSystem::::new_ref(); + + let a_var = BigIntVar::new_witness(cs.clone(), || { + Ok((a.clone(), Bound(lb.clone(), ub.clone()))) + })?; + + let a_const = BigIntVar::::constant(a.clone()); + + assert_eq!(a, a_var.value()?); + assert_eq!(a, a_const.value()?); + assert!(cs.is_satisfied()?); + } + } + + Ok(()) + } + + #[test] + fn test_mul_bigint() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let size = 2048; + + let rng = &mut test_rng(); + let a = rng.gen_bigint(size as u64); + let b = rng.gen_bigint(size as u64); + let ab = &a * &b; + let aab = &a * &ab; + let abb = &ab * &b; + + let a_var = BigIntVar::new_witness(cs.clone(), || { + Ok(( + a, + Bound( + BigInt::one() - (BigInt::one() << size), + (BigInt::one() << size) - BigInt::one(), + ), + )) + })?; + let b_var = BigIntVar::new_witness(cs.clone(), || { + Ok(( + b, + Bound( + BigInt::one() - (BigInt::one() << size), + (BigInt::one() << size) - BigInt::one(), + ), + )) + })?; + let ab_var = BigIntVar::new_witness(cs.clone(), || { + Ok(( + ab, + Bound( + BigInt::one() - (BigInt::one() << (size * 2)), + (BigInt::one() << (size * 2)) - BigInt::one(), + ), + )) + })?; + let aab_var = BigIntVar::new_witness(cs.clone(), || { + Ok(( + aab, + Bound( + BigInt::one() - (BigInt::one() << (size * 3)), + (BigInt::one() << (size * 3)) - BigInt::one(), + ), + )) + })?; + let abb_var = BigIntVar::new_witness(cs.clone(), || { + Ok(( + abb, + Bound( + BigInt::one() - (BigInt::one() << (size * 3)), + (BigInt::one() << (size * 3)) - BigInt::one(), + ), + )) + })?; + + a_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&ab_var)?; + a_var + .mul_unaligned(&ab_var)? + .enforce_equal_unaligned(&aab_var)?; + ab_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&abb_var)?; + + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_mul_fq() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let rng = &mut test_rng(); + let a = Fq::rand(rng); + let b = Fq::rand(rng); + let ab = a * b; + let aab = a * ab; + let abb = ab * b; + + let a_var = NonNativeFieldVar::::new_witness(cs.clone(), || Ok(a))?; + let b_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(b))?; + let ab_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(ab))?; + let aab_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(aab))?; + let abb_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(abb))?; + + a_var.mul_unaligned(&b_var)?.enforce_congruent(&ab_var)?; + a_var.mul_unaligned(&ab_var)?.enforce_congruent(&aab_var)?; + ab_var.mul_unaligned(&b_var)?.enforce_congruent(&abb_var)?; + + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_pow() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let rng = &mut test_rng(); + + let a = Fq::rand(rng); + + let a_var = NonNativeFieldVar::::new_witness(cs.clone(), || Ok(a))?; + + let mut r_var = a_var.clone(); + for _ in 0..16 { + r_var = r_var.mul_unaligned(&r_var)?.modulo()?; + } + r_var = r_var.mul_unaligned(&a_var)?.modulo()?; + assert_eq!( + BigInt::from_biguint(Sign::Plus, a.pow([65537u64]).into()), + r_var.value()? + ); + assert!(cs.is_satisfied()?); + Ok(()) + } + + #[test] + fn test_vec_vec_mul() -> Result<(), Box> { + let cs = ConstraintSystem::::new_ref(); + + let len = 1000; + + let rng = &mut test_rng(); + let a = (0..len).map(|_| Fq::rand(rng)).collect::>(); + let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); + let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); + + let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; + let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; + let c_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(c))?; + + let mut r_var: NonNativeFieldVar = + NonNativeFieldVar::constant(BigUint::zero().into()).into(); + for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { + r_var = r_var.add_unaligned(&a.mul_unaligned(&b)?)?; + } + r_var.enforce_congruent(&c_var)?; + + assert!(cs.is_satisfied()?); + Ok(()) + } +} diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs new file mode 100644 index 000000000..1f04e1472 --- /dev/null +++ b/crates/primitives/src/algebra/group/mod.rs @@ -0,0 +1,174 @@ +use ark_ec::{ + short_weierstrass::{Projective, SWCurveConfig}, + AffineRepr, CurveGroup, PrimeGroup, +}; +use ark_ff::{BigInteger, Field, One, PrimeField, Zero}; +use ark_r1cs_std::convert::ToBitsGadget; +use ark_r1cs_std::{ + alloc::AllocVar, + convert::ToConstraintFieldGadget, + fields::fp::FpVar, + groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, + prelude::Boolean, + GR1CSVar, +}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::mem::swap; +use num_bigint::{BigInt, BigUint, Sign}; +use num_integer::Integer; + +use crate::algebra::field::nonnative2::IntVarInner; +use crate::{ + algebra::field::{ + nonnative::{Bound, NonNativeUintVar}, + SonobeField, + }, + traits::{Inputize, InputizeNonNative}, + transcripts::{Absorbable, AbsorbableGadget}, +}; + +pub mod nonnative; + +pub type CF1 = ::ScalarField; +pub type CF2 = <::BaseField as Field>::BasePrimeField; +pub type CI1 = <::ScalarField as PrimeField>::BigInt; +pub type CI2 = <<::BaseField as Field>::BasePrimeField as PrimeField>::BigInt; + +/// `Curve` trait is a wrapper around `CurveGroup` that also includes the +/// necessary bounds for the curve to be used conveniently in folding schemes. +pub trait SonobeCurve: + CurveGroup + + Absorbable + + Inputize + + InputizeNonNative +{ + /// The in-circuit variable type for this curve. + type Var: CurveVar; +} + +impl> SonobeCurve + for Projective

+{ + type Var = ProjectiveVar>; +} + +impl>> Absorbable for Projective

{ + fn absorb_into(&self, dest: &mut Vec) { + let affine = self.into_affine(); + let (x, y) = affine.xy().unwrap_or_default(); + [x, y].absorb_into(dest); + } +} + +impl> AbsorbableGadget> + for ProjectiveVar> +{ + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + let mut vec = self.to_constraint_field()?; + // The last element in the vector tells whether the point is infinity, + // but we can in fact avoid absorbing it without loss of soundness. + // This is because the `to_constraint_field` method internally invokes + // [`ProjectiveVar::to_afine`](https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/groups/curves/short_weierstrass/mod.rs#L160-L195), + // which guarantees that an infinity point is represented as `(0, 0)`, + // but the y-coordinate of a non-infinity point is never 0 (for why, see + // https://crypto.stackexchange.com/a/108242 ). + vec.pop(); + dest.extend(vec); + Ok(()) + } +} + +impl> Inputize for Projective

{ + /// Returns the internal representation in the same order as how the value + /// is allocated in `ProjectiveVar::new_input`. + fn inputize(&self) -> Vec { + let affine = self.into_affine(); + match affine.xy() { + Some((x, y)) => vec![x, y, One::one()], + None => vec![Zero::zero(), One::one(), Zero::zero()], + } + } +} + +impl> + InputizeNonNative for Projective

+{ + /// Returns the internal representation in the same order as how the value + /// is allocated in `NonNativeAffineVar::new_input`. + fn inputize_nonnative(&self) -> Vec { + let affine = self.into_affine(); + let (x, y) = affine.xy().unwrap_or_default(); + + [x, y].inputize_nonnative() + } +} + +fn lattice_reduction_2x2( + mut b1: (BigInt, BigInt), + mut b2: (BigInt, BigInt), +) -> ((BigInt, BigInt), (BigInt, BigInt)) { + loop { + let mut b1_norm_sq = &b1.0 * &b1.0 + &b1.1 * &b1.1; + let mut b2_norm_sq = &b2.0 * &b2.0 + &b2.1 * &b2.1; + + if b1_norm_sq > b2_norm_sq { + swap(&mut b1, &mut b2); + swap(&mut b1_norm_sq, &mut b2_norm_sq); + } + + let (mut m, r) = (&b1.0 * &b2.0 + &b1.1 * &b2.1).div_rem(&b1_norm_sq); + if &r + &r >= b1_norm_sq { + m += BigInt::one(); + } + + if m.is_zero() { + break; + } + + b2.0 -= &m * &b1.0; + b2.1 -= &m * &b1.1; + } + + (b1, b2) +} + +pub trait PointScalarMulGadget: Sized { + fn mul_scalar(&self, scalar: &impl ToBitsGadget) -> Result; +} + +impl PointScalarMulGadget> for C { + fn mul_scalar(&self, scalar: &impl ToBitsGadget>) -> Result { + let scalar = scalar.to_bits_le()?; + + let cs = scalar.cs(); + + let m = BigInt::from_biguint(Sign::Plus, CF1::::MODULUS.into()); + let m_sqrt = m.sqrt(); + + let (a, b) = lattice_reduction_2x2( + (m, Zero::zero()), + ( + CI2::::from_bits_le(&scalar.value().unwrap_or_default()) + .into() + .into(), + One::one(), + ), + ) + .0; + let (a_sign, a_abs) = a.into_parts(); + let (b_sign, b_abs) = b.into_parts(); + let a_is_negative = + Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(a_sign == Sign::Minus))?; + let b_is_negative = + Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(b_sign == Sign::Minus))?; + + let a = NonNativeUintVar::new_variable_with_inferred_mode(cs.clone(), || { + Ok((a_abs.into(), Bound::new_ub(m_sqrt.clone()))) + })?; + let b = NonNativeUintVar::new_variable_with_inferred_mode(cs, || { + Ok((b_abs.into(), Bound::new_ub(m_sqrt))) + })?; + + todo!() + } +} diff --git a/crates/primitives/src/gadgets/nonnative/affine.rs b/crates/primitives/src/algebra/group/nonnative.rs similarity index 74% rename from crates/primitives/src/gadgets/nonnative/affine.rs rename to crates/primitives/src/algebra/group/nonnative.rs index f24bf8968..4663ba43d 100644 --- a/crates/primitives/src/gadgets/nonnative/affine.rs +++ b/crates/primitives/src/algebra/group/nonnative.rs @@ -1,5 +1,5 @@ use ark_ec::{short_weierstrass::SWFlags, AffineRepr}; -use ark_ff::PrimeField; +use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, eq::EqGadget, @@ -9,11 +9,12 @@ use ark_r1cs_std::{ }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; -use ark_std::{borrow::Borrow, Zero}; +use ark_std::borrow::Borrow; -use crate::traits::{AbsorbNonNativeGadget, SonobeCurve}; - -use super::uint::NonNativeUintVar; +use crate::{ + algebra::{field::nonnative::NonNativeUintVar, group::SonobeCurve}, + transcripts::AbsorbableGadget, +}; /// NonNativeAffineVar represents an elliptic curve point in Affine representation in the non-native /// field, over the constraint field. It is not intended to perform operations, but just to contain @@ -52,8 +53,8 @@ impl GR1CSVar for NonNativeAffineVar { } fn value(&self) -> Result { - let x = C::BaseField::from_le_bytes_mod_order(&self.x.value()?.to_bytes_le()); - let y = C::BaseField::from_le_bytes_mod_order(&self.y.value()?.to_bytes_le()); + let x = C::BaseField::from_le_bytes_mod_order(&self.x.value()?.magnitude().to_bytes_le()); + let y = C::BaseField::from_le_bytes_mod_order(&self.y.value()?.magnitude().to_bytes_le()); // Below is a workaround to convert the `x` and `y` coordinates to a // point. This is because the `SonobeCurve` trait does not provide a // method to construct a point from `BaseField` elements. @@ -83,47 +84,12 @@ impl GR1CSVar for NonNativeAffineVar { impl EqGadget for NonNativeAffineVar { fn is_eq(&self, other: &Self) -> Result, SynthesisError> { - let mut result = Boolean::TRUE; - if self.x.0.len() != other.x.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - if self.y.0.len() != other.y.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - for (l, r) in self - .x - .0 - .iter() - .chain(&self.y.0) - .zip(other.x.0.iter().chain(&other.y.0)) - { - if l.ub != r.ub { - return Err(SynthesisError::Unsatisfiable); - } - result &= l.v.is_eq(&r.v)?; - } - Ok(result) + Ok(self.x.is_eq(&other.x)? & self.y.is_eq(&other.y)?) } fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { - if self.x.0.len() != other.x.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - if self.y.0.len() != other.y.0.len() { - return Err(SynthesisError::Unsatisfiable); - } - for (l, r) in self - .x - .0 - .iter() - .chain(&self.y.0) - .zip(other.x.0.iter().chain(&other.y.0)) - { - if l.ub != r.ub { - return Err(SynthesisError::Unsatisfiable); - } - l.v.enforce_equal(&r.v)?; - } + self.x.enforce_equal(&other.x)?; + self.y.enforce_equal(&other.y)?; Ok(()) } } @@ -136,11 +102,9 @@ impl NonNativeAffineVar { } } -impl AbsorbNonNativeGadget for NonNativeAffineVar { - fn to_native_sponge_field_elements( - &self, - ) -> Result>, SynthesisError> { - [&self.x, &self.y].to_native_sponge_field_elements() +impl AbsorbableGadget> for NonNativeAffineVar { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + (&self.x, &self.y).absorb_into(dest) } } @@ -151,7 +115,10 @@ mod tests { use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, UniformRand}; - use crate::traits::{Absorbable, Inputize, InputizeNonNative}; + use crate::{ + traits::{Inputize, InputizeNonNative}, + transcripts::Absorbable, + }; use super::*; @@ -172,10 +139,7 @@ mod tests { let mut rng = ark_std::test_rng(); let p = Projective::rand(&mut rng); let p_var = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; - assert_eq!( - p_var.to_native_sponge_field_elements()?.value()?, - p.extract_absorbed() - ); + assert_eq!(p_var.to_absorbable()?.value()?, p.to_absorbable()); Ok(()) } @@ -188,7 +152,7 @@ mod tests { let cs = ConstraintSystem::::new_ref(); let p_var = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; assert_eq!( - [p_var.x.0.value()?, p_var.y.0.value()?].concat(), + [p_var.x.limbs.value()?, p_var.y.limbs.value()?].concat(), p.inputize_nonnative() ); diff --git a/crates/primitives/src/algebra/mod.rs b/crates/primitives/src/algebra/mod.rs new file mode 100644 index 000000000..82d6047fb --- /dev/null +++ b/crates/primitives/src/algebra/mod.rs @@ -0,0 +1,3 @@ +pub mod field; +pub mod group; +pub mod ops; diff --git a/crates/primitives/src/algebra/ops/bits.rs b/crates/primitives/src/algebra/ops/bits.rs new file mode 100644 index 000000000..1688a227c --- /dev/null +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -0,0 +1,33 @@ +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::{alloc::AllocVar, boolean::Boolean, eq::EqGadget, fields::fp::FpVar, GR1CSVar}; +use ark_relations::gr1cs::SynthesisError; + +pub trait FromBitsGadget: Sized { + fn from_bits_le(bits: &[Boolean]) -> Result; +} + +pub trait ToBitsGadgetExt: Sized { + fn to_n_bits_le(&self, n: usize) -> Result>, SynthesisError>; + + fn enforce_bit_length(&self, n: usize) -> Result<(), SynthesisError> { + self.to_n_bits_le(n)?; + Ok(()) + } +} +impl FromBitsGadget for FpVar { + fn from_bits_le(bits: &[Boolean]) -> Result { + Boolean::le_bits_to_fp(bits) + } +} + +impl ToBitsGadgetExt for FpVar { + fn to_n_bits_le(&self, n: usize) -> Result>, SynthesisError> { + let mut bits = self.value().unwrap_or_default().into_bigint().to_bits_le(); + bits.resize(n, false); + let bits = Vec::new_variable_with_inferred_mode(self.cs(), || Ok(bits))?; + + Boolean::le_bits_to_fp(&bits)?.enforce_equal(self)?; + + Ok(bits) + } +} diff --git a/crates/primitives/src/gadgets/math/eq.rs b/crates/primitives/src/algebra/ops/eq.rs similarity index 100% rename from crates/primitives/src/gadgets/math/eq.rs rename to crates/primitives/src/algebra/ops/eq.rs diff --git a/crates/primitives/src/gadgets/math/matrix.rs b/crates/primitives/src/algebra/ops/matrix.rs similarity index 100% rename from crates/primitives/src/gadgets/math/matrix.rs rename to crates/primitives/src/algebra/ops/matrix.rs diff --git a/crates/primitives/src/gadgets/math/mod.rs b/crates/primitives/src/algebra/ops/mod.rs similarity index 61% rename from crates/primitives/src/gadgets/math/mod.rs rename to crates/primitives/src/algebra/ops/mod.rs index c4e275c5a..2646ce8c0 100644 --- a/crates/primitives/src/gadgets/math/mod.rs +++ b/crates/primitives/src/algebra/ops/mod.rs @@ -1,3 +1,5 @@ +pub mod bits; pub mod eq; pub mod matrix; +pub mod rlc; pub mod vector; diff --git a/crates/primitives/src/algebra/ops/rlc.rs b/crates/primitives/src/algebra/ops/rlc.rs new file mode 100644 index 000000000..3c94d01f6 --- /dev/null +++ b/crates/primitives/src/algebra/ops/rlc.rs @@ -0,0 +1,44 @@ +use ark_std::{ + iter::Sum, + ops::{Add, Mul}, +}; + +pub trait ScalarRLC { + type Value; + + fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value; +} + +impl ScalarRLC for I +where + I::Item: Add + Sum + for<'a> Mul<&'a Coeff, Output = I::Item>, +{ + type Value = I::Item; + + fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value { + self.zip(coeffs).map(|(v, c)| v * c).sum::() + } +} + +pub trait SliceRLC { + type Value; + + fn slice_rlc(self, coeffs: &[Coeff]) -> Vec; +} + +impl<'a, T: 'a, I: Iterator, Coeff> SliceRLC for I +where + T: Add + Copy, + for<'x> T: Mul<&'x Coeff, Output = T>, +{ + type Value = T; + + fn slice_rlc(self, coeffs: &[Coeff]) -> Vec { + let mut iter = self.zip(coeffs).map(|(v, c)| v.iter().map(|x| *x * c)); + let first = iter.next().unwrap(); + + iter.fold(first.collect(), |acc, v| { + acc.into_iter().zip(v).map(|(a, b)| a + b).collect() + }) + } +} diff --git a/crates/primitives/src/gadgets/math/vector.rs b/crates/primitives/src/algebra/ops/vector.rs similarity index 100% rename from crates/primitives/src/gadgets/math/vector.rs rename to crates/primitives/src/algebra/ops/vector.rs diff --git a/crates/primitives/src/arithmetizations/ccs/circuits.rs b/crates/primitives/src/arithmetizations/ccs/circuits.rs index 60fec5c33..6877f48d9 100644 --- a/crates/primitives/src/arithmetizations/ccs/circuits.rs +++ b/crates/primitives/src/arithmetizations/ccs/circuits.rs @@ -6,7 +6,7 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::borrow::Borrow; -use crate::gadgets::math::matrix::SparseMatrixVar; +use crate::algebra::ops::matrix::SparseMatrixVar; use super::CCS; diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 8418f8a4b..9fa101602 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -1,7 +1,7 @@ use ark_ff::Field; use ark_poly::DenseMultilinearExtension; use ark_relations::gr1cs::Matrix; -use ark_std::{cfg_into_iter, log2}; +use ark_std::{cfg_into_iter, cfg_iter, log2}; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -96,12 +96,11 @@ impl CCS { pub fn mle( &self, i: usize, - z: Assignments>, + z: Assignments + Sync>, ) -> DenseMultilinearExtension { DenseMultilinearExtension { num_vars: self.s, - evaluations: self.M[i] - .iter() + evaluations: cfg_iter!(self.M[i]) .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) .chain(vec![F::zero(); (1 << self.s) - self.m]) .collect(), diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index f31fd632f..afeec6272 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -4,13 +4,13 @@ use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::{borrow::Borrow, marker::PhantomData, One}; use crate::{ - arithmetizations::ArithRelationGadget, - circuits::Assignments, - gadgets::math::{ + algebra::ops::{ eq::EquivalenceGadget, matrix::{MatrixGadget, SparseMatrixVar}, vector::VectorGadget, }, + arithmetizations::ArithRelationGadget, + circuits::Assignments, }; use super::R1CS; diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index 593ba7879..18da7f641 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,3 +1,6 @@ +use ark_ff::Field; +use ark_r1cs_std::alloc::AllocVar; +use ark_relations::gr1cs::SynthesisError; use ark_std::{ fmt::Debug, iter::Sum, @@ -12,7 +15,7 @@ pub mod pedersen; #[derive(Debug, Error)] pub enum Error { // Commitment errors - #[error("The message being committed to has length {1}, which exceeds the maximum supported length of {0}")] + #[error("The message being committed to has length {1}, exceeding the maximum supported length of {0}")] MessageTooLong(usize, usize), #[error("Blinding factor not 0 for Commitment without hiding")] BlindingNotZero, @@ -57,6 +60,77 @@ pub trait VectorCommitment: 'static + Debug + PartialEq { ) -> Result; } +pub trait VectorCommitmentGadget { + type Native: VectorCommitment; + + type KeyVar; + type ScalarVar: Clone + + Add + + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> + + Mul + + for<'a> Mul<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar>; + type IntermediateScalarVar: Clone + + TryInto + + Add + + for<'a> Add<&'a Self::IntermediateScalarVar, Output = Self::IntermediateScalarVar> + + Mul + + for<'a> Mul<&'a Self::IntermediateScalarVar, Output = Self::IntermediateScalarVar> + + Add + + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> + + Mul + + for<'a> Mul<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar>; + type CommitmentVar: Clone; + type RandomnessVar; + + fn open( + ck: &Self::KeyVar, + v: &[Self::ScalarVar], + r: &Self::RandomnessVar, + cm: &Self::CommitmentVar, + ) -> Result<(), SynthesisError>; +} + +#[derive(Clone, Copy, Default, Debug)] +pub struct Null; + +impl Add for Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Add for &Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Mul for Null { + type Output = Self; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Mul for &Null { + type Output = Null; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Sum for Null { + fn sum>(_: I) -> Self { + Null + } +} + #[cfg(test)] mod tests { use ark_ff::UniformRand; diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 3d53d7a61..1716a1d53 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -1,9 +1,30 @@ -use ark_r1cs_std::{boolean::Boolean, convert::ToBitsGadget, groups::CurveVar}; +use ark_ec::{ + short_weierstrass::{Projective, SWCurveConfig}, + CurveGroup, +}; +use ark_ff::{AdditiveGroup, PrimeField}; +use ark_r1cs_std::{ + boolean::Boolean, + convert::ToBitsGadget, + eq::EqGadget, + fields::{fp::FpVar, FieldVar}, + groups::{ + curves::short_weierstrass::{non_zero_affine::NonZeroAffineVar, ProjectiveVar}, + CurveVar, + }, + GR1CSVar, +}; use ark_relations::gr1cs::SynthesisError; use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; use super::{Error, VectorCommitment}; -use crate::traits::{Null, SonobeCurve, CF2}; +use crate::{ + algebra::field::{nonnative::NonNativeUintVar, nonnative2::IntVarInner}, + commitments::{Null, VectorCommitmentGadget}, + traits::{CF2, SonobeCurve}, +}; +use crate::algebra::field::nonnative2::NonNativeFieldVar; +use crate::traits::CF1; #[derive(Debug, PartialEq)] pub struct Pedersen { @@ -88,21 +109,99 @@ impl VectorCommitment for Pedersen { } } -pub struct PedersenGadget { +pub struct PedersenGadget { _c: PhantomData, } +// fn joint_scalar_mul_be>( +// p: &Projective

, +// q: &Projective

, +// bits1: impl Iterator>, +// bits2: impl Iterator>, +// ) -> Result>, SynthesisError> { +// // prepare bits decomposition +// let mut bits1 = bits1.collect::>(); +// if bits1.len() == 0 { +// return Ok(ProjectiveVar::zero()); +// } +// // Remove unnecessary constant zeros in the most-significant positions. +// bits1 = bits1 +// .into_iter() +// // We iterate from the MSB down. +// .rev() +// // Skip leading zeros, if they are constants. +// .skip_while(|b| b.is_constant() && (b.value().unwrap() == false)) +// .collect(); + +// let mut bits2 = bits2.collect::>(); +// if bits2.len() == 0 { +// return Ok(ProjectiveVar::zero()); +// } +// // Remove unnecessary constant zeros in the most-significant positions. +// bits2 = bits2 +// .into_iter() +// // We iterate from the MSB down. +// .rev() +// // Skip leading zeros, if they are constants. +// .skip_while(|b| b.is_constant() && (b.value().unwrap() == false)) +// .collect(); + +// let acc = p.double().into_affine(); +// let sum = (p + q).into_affine(); +// let diff = (p - q).into_affine(); + +// let (sum_x, sum_y) = (FpVar::Constant(sum.x), FpVar::Constant(sum.y)); +// let (diff_x, diff_y) = (FpVar::Constant(diff.x), FpVar::Constant(diff.y)); +// let (mut x, mut y) = (FpVar::Constant(acc.x), FpVar::Constant(acc.y)); + +// // double-and-add loop +// for (bit1, bit2) in (bits1.iter().rev().skip(1).rev()).zip(bits2.iter().rev().skip(1).rev()) { +// let xor = *bit1 ^ *bit2; +// let xx = xor.select(&diff_x, &sum_x)?; +// let yy = xor.select(&diff_y, &sum_y)?; +// let yy = bit1.select(&yy, &yy.negate()?)?; + +// if [&x, &y].is_constant() || ([&xx, &yy].is_constant()) { +// let p = NonZeroAffineVar::new(x.clone(), y.clone()) +// .double()? +// .add_unchecked(&NonZeroAffineVar::new(xx, yy))?; +// x = p.x; +// y = p.y; +// } else { +// let lambda_1 = (&yy - &y).mul_by_inverse_unchecked(&(&xx - &x))?; +// let lambda_1_square = lambda_1.square()?; + +// let lambda_2 = y +// .mul_by_inverse_unchecked(&(&x.double()? + &xx - &lambda_1_square))? +// .double()? +// - lambda_1; + +// let x4 = lambda_2.square()? - lambda_1_square + &xx; +// let y4 = lambda_2 * &(&x - &x4) - &y; +// x = x4; +// y = y4; +// }; +// } + +// let mut acc = NonZeroAffineVar::new(x, y); +// // last bit +// aff1_neg = aff1_neg.add_unchecked(&acc)?; +// acc = bits1[bits1.len() - 1].select(&acc, &aff1_neg)?; +// aff2_neg = aff2_neg.add_unchecked(&acc)?; +// acc = bits2[bits1.len() - 1].select(&acc, &aff2_neg)?; + +// acc.into_projective().add_mixed(&{ +// let mut p = diff; +// for _ in 0..bits1.len() - 1 { +// p = p.double()?; +// } +// NonZeroAffineVar::new(p.x, p.y.negate()?) +// }) +// } + impl PedersenGadget { - pub fn commit( - h: &C::Var, - g: &[C::Var], - v: &[Vec>>], - r: &[Boolean>], - ) -> Result { + fn msm(g: &[C::Var], v: &[Vec>>]) -> Result { let mut res = C::Var::zero(); - if H { - res += h.scalar_mul_le(r.iter())?; - } let n = v.len(); if n % 2 == 1 { res += g[n - 1].scalar_mul_le(v[n - 1].to_bits_le()?.iter())?; @@ -124,6 +223,65 @@ impl PedersenGadget { } } +impl VectorCommitmentGadget for PedersenGadget { + type Native = Pedersen; + + type KeyVar = Vec; + + type ScalarVar = NonNativeFieldVar, CF1, true>; + + type IntermediateScalarVar = NonNativeFieldVar, CF1, false>; + + type CommitmentVar = C::Var; + + type RandomnessVar = (); + + fn open( + ck: &Self::KeyVar, + v: &[Self::ScalarVar], + _r: &Self::RandomnessVar, + cm: &Self::CommitmentVar, + ) -> Result<(), SynthesisError> { + Self::msm( + ck, + &v.iter() + .map(|i| i.to_bits_le()) + .collect::, _>>()?, + )? + .enforce_equal(cm) + } +} + +impl VectorCommitmentGadget for PedersenGadget { + type Native = Pedersen; + + type KeyVar = (Vec, C::Var); + + type ScalarVar = NonNativeFieldVar, CF1, true>; + + type IntermediateScalarVar = NonNativeFieldVar, CF1, false>; + + type CommitmentVar = C::Var; + + type RandomnessVar = NonNativeFieldVar, CF1, true>; + + fn open( + (g, h): &Self::KeyVar, + v: &[Self::ScalarVar], + r: &Self::RandomnessVar, + cm: &Self::CommitmentVar, + ) -> Result<(), SynthesisError> { + let gv = Self::msm( + g, + &v.iter() + .map(|i| i.to_bits_le()) + .collect::, _>>()?, + )?; + let hr = h.scalar_mul_le(r.to_bits_le()?.iter())?; + (gv + hr).enforce_equal(cm) + } +} + #[cfg(test)] mod tests { use ark_bn254::G1Projective; diff --git a/crates/primitives/src/gadgets/mod.rs b/crates/primitives/src/gadgets/mod.rs deleted file mode 100644 index 89346e75c..000000000 --- a/crates/primitives/src/gadgets/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod math; -pub mod nonnative; diff --git a/crates/primitives/src/gadgets/nonnative/mod.rs b/crates/primitives/src/gadgets/nonnative/mod.rs deleted file mode 100644 index 497b9870f..000000000 --- a/crates/primitives/src/gadgets/nonnative/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod affine; -pub mod uint; diff --git a/crates/primitives/src/gadgets/nonnative/uint.rs b/crates/primitives/src/gadgets/nonnative/uint.rs deleted file mode 100644 index fa0db0e69..000000000 --- a/crates/primitives/src/gadgets/nonnative/uint.rs +++ /dev/null @@ -1,993 +0,0 @@ -use ark_ff::{BigInteger, One, PrimeField, Zero}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - convert::ToBitsGadget, - fields::{fp::FpVar, FieldVar}, - prelude::EqGadget, - select::CondSelectGadget, - GR1CSVar, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{ - borrow::Borrow, - cmp::{max, min}, - ops::Index, -}; -use num_bigint::BigUint; -use num_integer::Integer; - -use crate::{ - gadgets::math::{ - eq::EquivalenceGadget, - matrix::{MatrixGadget, SparseMatrixVar}, - vector::VectorGadget, - }, - traits::{AbsorbNonNativeGadget, SonobeField}, -}; - -/// `LimbVar` represents a single limb of a non-native unsigned integer in the -/// circuit. -/// The limb value `v` should be small enough to fit into `FpVar`, and we also -/// store an upper bound `ub` for the limb value, which is treated as a constant -/// in the circuit and is used for efficient equality checks and some arithmetic -/// operations. -#[derive(Debug, Clone)] -pub struct LimbVar { - pub v: FpVar, - pub ub: BigUint, -} - -impl]>> From for LimbVar { - fn from(bits: B) -> Self { - Self { - // `Boolean::le_bits_to_fp` will return an error if the internal - // invocation of `Boolean::enforce_in_field_le` fails. - // However, this method is only called when the length of `bits` is - // greater than `F::MODULUS_BIT_SIZE`, which should not happen in - // our case where `bits` is guaranteed to be short. - v: Boolean::le_bits_to_fp(bits.as_ref()).unwrap(), - ub: (BigUint::one() << bits.as_ref().len()) - BigUint::one(), - } - } -} - -impl Default for LimbVar { - fn default() -> Self { - Self { - v: FpVar::zero(), - ub: BigUint::zero(), - } - } -} - -impl GR1CSVar for LimbVar { - type Value = F; - - fn cs(&self) -> ConstraintSystemRef { - self.v.cs() - } - - fn value(&self) -> Result { - self.v.value() - } -} - -impl CondSelectGadget for LimbVar { - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - // We only allow selecting between two values with the same upper bound - assert_eq!(true_value.ub, false_value.ub); - Ok(Self { - v: cond.select(&true_value.v, &false_value.v)?, - ub: true_value.ub.clone(), - }) - } -} - -impl LimbVar { - /// Add two `LimbVar`s. - /// Returns `None` if the upper bound of the sum is too large, i.e., - /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. - /// Otherwise, returns the sum as a `LimbVar`. - pub fn add(&self, other: &Self) -> Option { - let ubound = &self.ub + &other.ub; - if ubound < F::MODULUS_MINUS_ONE_DIV_TWO.into() { - Some(Self { - v: &self.v + &other.v, - ub: ubound, - }) - } else { - None - } - } - - /// Add multiple `LimbVar`s. - /// Returns `None` if the upper bound of the sum is too large, i.e., - /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. - /// Otherwise, returns the sum as a `LimbVar`. - pub fn add_many(limbs: &[Self]) -> Option { - let ubound = limbs.iter().map(|l| &l.ub).sum(); - if ubound < F::MODULUS_MINUS_ONE_DIV_TWO.into() { - Some(Self { - v: if limbs.is_constant() { - FpVar::constant(limbs.value().unwrap_or_default().into_iter().sum()) - } else { - limbs.iter().map(|l| &l.v).sum() - }, - ub: ubound, - }) - } else { - None - } - } - - /// Multiply two `LimbVar`s. - /// Returns `None` if the upper bound of the product is too large, i.e., - /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. - /// Otherwise, returns the product as a `LimbVar`. - pub fn mul(&self, other: &Self) -> Option { - let ubound = &self.ub * &other.ub; - if ubound < F::MODULUS_MINUS_ONE_DIV_TWO.into() { - Some(Self { - v: &self.v * &other.v, - ub: ubound, - }) - } else { - None - } - } - - pub fn zero() -> Self { - Self::default() - } - - pub fn constant(v: F) -> Self { - Self { - v: FpVar::constant(v), - ub: v.into(), - } - } -} - -impl ToBitsGadget for LimbVar { - fn to_bits_le(&self) -> Result>, SynthesisError> { - let cs = self.cs(); - - let bits = &self - .v - .value() - .unwrap_or_default() - .into_bigint() - .to_bits_le()[..self.ub.bits() as usize]; - let bits = if cs.is_none() { - Vec::new_constant(cs, bits)? - } else { - Vec::new_witness(cs, || Ok(bits))? - }; - - Boolean::le_bits_to_fp(&bits)?.enforce_equal(&self.v)?; - - Ok(bits) - } -} - -/// `NonNativeUintVar` represents a non-native unsigned integer (BigUint) in the -/// circuit. -/// We apply [xJsnark](https://akosba.github.io/papers/xjsnark.pdf)'s techniques -/// for efficient operations on `NonNativeUintVar`. -/// Note that `NonNativeUintVar` is different from arkworks' `NonNativeFieldVar` -/// in that the latter runs the expensive `reduce` (`align` + `modulo` in our -/// terminology) after each arithmetic operation, while the former only reduces -/// the integer when explicitly called. -#[derive(Debug, Clone)] -pub struct NonNativeUintVar(pub Vec>); - -impl NonNativeUintVar { - pub const fn bits_per_limb() -> usize { - assert!(F::MODULUS_BIT_SIZE > 250); - // For a `F` with order > 250 bits, 55 is chosen for optimizing the most - // expensive part `Az∘Bz` when checking the R1CS relation for CycleFold. - // Consider using `NonNativeUintVar` to represent the base field `Fq`. - // Since 250 / 55 = 4.46, the `NonNativeUintVar` has 5 limbs. - // Now, the multiplication of two `NonNativeUintVar`s has 9 limbs, and - // each limb has at most 2^{55 * 2} * 5 = 112.3 bits. - // For a 1400x1400 matrix `A`, the multiplication of `A`'s row and `z` - // is the sum of 1400 `NonNativeUintVar`s, each with 9 limbs. - // Thus, the maximum bit length of limbs of each element in `Az` is - // 2^{55 * 2} * 5 * 1400 = 122.7 bits. - // Finally, in the hadamard product of `Az` and `Bz`, every element has - // 17 limbs, whose maximum bit length is (2^{55 * 2} * 5 * 1400)^2 * 9 - // = 248.7 bits and is less than the native field `Fr`. - // Thus, 55 allows us to compute `Az∘Bz` without the expensive alignment - // operation. - // - // TODO: either make it a global const, or compute an optimal value - // based on the modulus size. - 55 - } -} - -struct BoundedBigUint(BigUint, usize); - -impl AllocVar for NonNativeUintVar { - fn new_variable>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let cs = cs.into().cs(); - let v = f()?; - let BoundedBigUint(x, l) = v.borrow(); - - let mut limbs = vec![]; - for chunk in (0..*l) - .map(|i| x.bit(i as u64)) - .collect::>() - .chunks(Self::bits_per_limb()) - { - let limb = F::from(F::BigInt::from_bits_le(chunk)); - let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; - Self::enforce_bit_length(&limb, chunk.len())?; - limbs.push(LimbVar { - v: limb, - ub: (BigUint::one() << chunk.len()) - BigUint::one(), - }); - } - - Ok(Self(limbs)) - } -} - -impl AllocVar for NonNativeUintVar { - fn new_variable>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let cs = cs.into().cs(); - let v = f()?; - assert_eq!(G::extension_degree(), 1); - // `unwrap` is safe because `G` is a field with extension degree 1, and - // thus `G::to_base_prime_field_elements` should return an iterator with - // exactly one element. - let v = v.borrow().to_base_prime_field_elements().next().unwrap(); - - let mut limbs = vec![]; - - for chunk in v.into_bigint().to_bits_le().chunks(Self::bits_per_limb()) { - let limb = F::from(F::BigInt::from_bits_le(chunk)); - let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; - Self::enforce_bit_length(&limb, chunk.len())?; - limbs.push(LimbVar { - v: limb, - ub: (BigUint::one() << chunk.len()) - BigUint::one(), - }); - } - - Ok(Self(limbs)) - } -} - -impl GR1CSVar for NonNativeUintVar { - type Value = BigUint; - - fn cs(&self) -> ConstraintSystemRef { - self.0.cs() - } - - fn value(&self) -> Result { - let mut r = BigUint::zero(); - - for limb in self.0.value()?.into_iter().rev() { - r <<= Self::bits_per_limb(); - r += Into::::into(limb); - } - - Ok(r) - } -} - -impl NonNativeUintVar { - /// Enforce `self` to be less than `other`, where `self` and `other` should - /// be aligned. - /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 - pub fn enforce_lt(&self, other: &Self) -> Result<(), SynthesisError> { - let len = max(self.0.len(), other.0.len()); - let zero = LimbVar::zero(); - - // Compute the difference between limbs of `other` and `self`. - // Denote a positive limb by `+`, a negative limb by `-`, a zero limb by - // `0`, and an unknown limb by `?`. - // Then, for `self < other`, `delta` should look like: - // ? ? ... ? ? + 0 0 ... 0 0 - let delta = (0..len) - .map(|i| { - let x = &self.0.get(i).unwrap_or(&zero).v; - let y = &other.0.get(i).unwrap_or(&zero).v; - y - x - }) - .collect::>(); - - // `helper` is a vector of booleans that indicates if the corresponding - // limb of `delta` is the first (searching from MSB) positive limb. - // For example, if `delta` is: - // - + ... + - + 0 0 ... 0 0 - // <---- search in this direction -------- - // Then `helper` should be: - // F F ... F F T F F ... F F - let helper = { - let cs = self.cs().or(other.cs()); - let mut helper = vec![false; len]; - for i in (0..len).rev() { - let delta = delta[i].value().unwrap_or_default().into_bigint(); - if !delta.is_zero() && delta < F::MODULUS_MINUS_ONE_DIV_TWO { - helper[i] = true; - break; - } - } - if cs.is_none() { - Vec::>::new_constant(cs, helper)? - } else { - Vec::new_witness(cs, || Ok(helper))? - } - }; - - // `p` is the first positive limb in `delta`. - let mut p = FpVar::::zero(); - // `r` is the sum of all bits in `helper`, which should be 1 when `self` - // is less than `other`, as there should be more than one positive limb - // in `delta`, and thus exactly one true bit in `helper`. - let mut r = FpVar::zero(); - for (b, d) in helper.into_iter().zip(delta) { - // Choose the limb `d` only if `b` is true. - p += b.select(&d, &FpVar::zero())?; - // Either `r` or `d` should be zero. - // Consider the same example as above: - // - + ... + - + 0 0 ... 0 0 - // F F ... F F T F F ... F F - // |-----------| - // `r = 0` in this range (before/when we meet the first positive limb) - // |---------| - // `d = 0` in this range (after we meet the first positive limb) - // This guarantees that for every bit after the true bit in `helper`, - // the corresponding limb in `delta` is zero. - (&r * &d).enforce_equal(&FpVar::zero())?; - // Add the current bit to `r`. - r += FpVar::from(b); - } - - // Ensure that `r` is exactly 1. This guarantees that there is exactly - // one true value in `helper`. - r.enforce_equal(&FpVar::one())?; - // Ensure that `p` is positive, i.e., - // `0 <= p - 1 < 2^bits_per_limb < F::MODULUS_MINUS_ONE_DIV_TWO`. - // This guarantees that the true value in `helper` corresponds to a - // positive limb in `delta`. - Self::enforce_bit_length(&(p - FpVar::one()), Self::bits_per_limb())?; - - Ok(()) - } - - /// Enforce `self` to be equal to `other`, where `self` and `other` are not - /// necessarily aligned. - /// - /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L562-L798 - /// Similar implementations can also be found in https://github.com/alex-ozdemir/bellman-bignat/blob/0585b9d90154603a244cba0ac80b9aafe1d57470/src/mp/bignat.rs#L566-L661 - /// and https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/fields/emulated_fp/reduce.rs#L201-L323 - pub fn enforce_equal_unaligned(&self, other: &Self) -> Result<(), SynthesisError> { - let len = min(self.0.len(), other.0.len()); - - // Group the limbs of `self` and `other` so that each group nearly - // reaches the capacity `F::MODULUS_MINUS_ONE_DIV_TWO`. - // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` - // is the initial number of bits in a limb, just as what we do in grade - // school arithmetic, e.g., - // 5 9 - // x 7 3 - // ------------- - // 15 27 - // 35 63 - // ------------- <- When grouping 35, 15 + 63, and 27, we are computing - // 4 3 0 7 35 * 100 + (15 + 63) * 10 + 27 = 4307 - // Note that this is different from the concatenation `x_0 || x_1 ...`, - // since the bit-length of each limb is not necessarily the initial size - // `W`. - let (steps, x, y, rest) = { - // `steps` stores the size of each grouped limb. - let mut steps = vec![]; - // `x_grouped` stores the grouped limbs of `self`. - let mut x_grouped = vec![]; - // `y_grouped` stores the grouped limbs of `other`. - let mut y_grouped = vec![]; - let mut i = 0; - while i < len { - let mut j = i; - // The current grouped limbs of `self` and `other`. - let mut xx = LimbVar::zero(); - let mut yy = LimbVar::zero(); - while j < len { - let shift = BigUint::one() << (Self::bits_per_limb() * (j - i)); - assert!(shift < F::MODULUS_MINUS_ONE_DIV_TWO.into()); - let shift = LimbVar::constant(shift.into()); - match ( - // Try to group `x` and `y` into `xx` and `yy`. - self.0[j].mul(&shift).and_then(|x| xx.add(&x)), - other.0[j].mul(&shift).and_then(|y| yy.add(&y)), - ) { - // Update the result if successful. - (Some(x), Some(y)) => (xx, yy) = (x, y), - // Break the loop if the upper bound of the result exceeds - // the maximum capacity. - _ => break, - } - j += 1; - } - // Store the grouped limbs and their size. - steps.push((j - i) * Self::bits_per_limb()); - x_grouped.push(xx); - y_grouped.push(yy); - // Start the next group - i = j; - } - let remaining_limbs = &(if i < self.0.len() { self } else { other }).0[i..]; - let rest = if remaining_limbs.is_empty() { - FpVar::zero() - } else { - // If there is any remaining limb, the first one should be the - // final carry (which will be checked later), and the following - // ones should be zero. - - // Enforce the remaining limbs to be zero. - // Instead of doing that one by one, we check if their sum is - // zero using a single constraint. - // This is sound, as the upper bounds of the limbs and their sum - // are guaranteed to be less than `F::MODULUS_MINUS_ONE_DIV_TWO` - // (i.e., all of them are "non-negative"), implying that all - // limbs should be zero to make the sum zero. - LimbVar::add_many(&remaining_limbs[1..]) - .ok_or(SynthesisError::Unsatisfiable)? - .v - .enforce_equal(&FpVar::zero())?; - remaining_limbs[0].v.clone() - }; - (steps, x_grouped, y_grouped, rest) - }; - let n = steps.len(); - // `c` stores the current carry of `x_i - y_i` - let mut c = FpVar::::zero(); - // For each group, check the last `step_i` bits of `x_i` and `y_i` are - // equal. - // The intuition is to check `diff = x_i - y_i = 0 (mod 2^step_i)`. - // However, this is only true for `i = 0`, and we need to consider carry - // values `diff >> step_i` for `i > 0`. - // Therefore, we actually check `diff = x_i - y_i + c = 0 (mod 2^step_i)` - // and derive the next `c` by computing `diff >> step_i`. - // To enforce `diff = 0 (mod 2^step_i)`, we compute `diff / 2^step_i` - // and enforce it to be small (soundness holds because for `a` that does - // not divide `b`, `b / a` in the field will be very large. - for i in 0..n { - let step = steps[i]; - c = (&x[i].v - &y[i].v + &c) - .mul_by_inverse_unchecked(&FpVar::constant(F::from(BigUint::one() << step)))?; - if i != n - 1 { - // Unlike the code mentioned above which add some offset to the - // diff `x_i - y_i + c` to make it always positive, we directly - // check if the absolute value of the diff is small. - Self::enforce_abs_bit_length( - &c, - (max(&x[i].ub, &y[i].ub).bits() as usize) - .checked_sub(step) - .unwrap_or_default(), - )?; - } else { - // For the final carry, we need to ensure that it equals the - // remaining limb `rest`. - c.enforce_equal(&rest)?; - } - } - - Ok(()) - } -} - -impl ToBitsGadget for NonNativeUintVar { - fn to_bits_le(&self) -> Result>, SynthesisError> { - Ok(self - .0 - .iter() - .map(|limb| limb.to_bits_le()) - .collect::, _>>()? - .concat()) - } -} - -impl CondSelectGadget for NonNativeUintVar { - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - assert_eq!(true_value.0.len(), false_value.0.len()); - let mut v = vec![]; - for i in 0..true_value.0.len() { - v.push(cond.select(&true_value.0[i], &false_value.0[i])?); - } - Ok(Self(v)) - } -} - -impl NonNativeUintVar { - pub fn ubound(&self) -> BigUint { - let mut r = BigUint::zero(); - - for i in self.0.iter().rev() { - r <<= Self::bits_per_limb(); - r += &i.ub; - } - - r - } - - fn enforce_bit_length(x: &FpVar, length: usize) -> Result>, SynthesisError> { - let cs = x.cs(); - - let bits = &x.value().unwrap_or_default().into_bigint().to_bits_le()[..length]; - let bits = if cs.is_none() { - Vec::new_constant(cs, bits)? - } else { - Vec::new_witness(cs, || Ok(bits))? - }; - - Boolean::le_bits_to_fp(&bits)?.enforce_equal(x)?; - - Ok(bits) - } - - fn enforce_abs_bit_length( - x: &FpVar, - length: usize, - ) -> Result>, SynthesisError> { - let cs = x.cs(); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - - let is_neg = Boolean::new_variable( - cs.clone(), - || Ok(x.value().unwrap_or_default().into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO), - mode, - )?; - let bits = Vec::new_variable( - cs.clone(), - || { - Ok({ - let x = x.value().unwrap_or_default(); - let mut bits = if is_neg.value().unwrap_or_default() { - -x - } else { - x - } - .into_bigint() - .to_bits_le(); - bits.resize(length, false); - bits - }) - }, - mode, - )?; - - // Below is equivalent to but more efficient than - // `Boolean::le_bits_to_fp(&bits)?.enforce_equal(&is_neg.select(&x.negate()?, &x)?)?` - // Note that this enforces: - // 1. The claimed absolute value `is_neg.select(&x.negate()?, &x)?` has - // exactly `length` bits. - // 2. `is_neg` is indeed the sign of `x`, i.e., `is_neg = false` when - // `0 <= x < (|F| - 1) / 2`, and `is_neg = true` when - // `(|F| - 1) / 2 <= x < F`, thus the claimed absolute value is - // correct. - // If `is_neg` is incorrect, then: - // a. `0 <= x < (|F| - 1) / 2`, but `is_neg = true`, then - // `is_neg.select(&x.negate()?, &x)?` returns `|F| - x`, - // which is greater than `(|F| - 1) / 2` and cannot fit in - // `length` bits (given that `length` is small). - // b. `(|F| - 1) / 2 <= x < F`, but `is_neg = false`, then - // `is_neg.select(&x.negate()?, &x)?` returns `x`, which is - // greater than `(|F| - 1) / 2` and cannot fit in `length` - // bits. - FpVar::from(is_neg).mul_equals(&x.double()?, &(x - Boolean::le_bits_to_fp(&bits)?))?; - - Ok(bits) - } - - /// Compute `self + other`, without aligning the limbs. - pub fn add_no_align(&self, other: &Self) -> Result { - let mut z = vec![LimbVar::zero(); max(self.0.len(), other.0.len())]; - for (i, v) in self.0.iter().enumerate() { - z[i] = z[i].add(v).ok_or(SynthesisError::Unsatisfiable)?; - } - for (i, v) in other.0.iter().enumerate() { - z[i] = z[i].add(v).ok_or(SynthesisError::Unsatisfiable)?; - } - Ok(Self(z)) - } - - /// Compute `self * other`, without aligning the limbs. - /// Implements the O(n) approach described in xJsnark, Section IV.B.1) - pub fn mul_no_align(&self, other: &Self) -> Result { - let len = self.0.len() + other.0.len() - 1; - if self.is_constant() || other.is_constant() { - // Use the naive approach for constant operands, which costs no - // constraints. - let z = (0..len) - .map(|i| { - let start = max(i + 1, other.0.len()) - other.0.len(); - let end = min(i + 1, self.0.len()); - LimbVar::add_many( - &(start..end) - .map(|j| self.0[j].mul(&other.0[i - j])) - .collect::>>()?, - ) - }) - .collect::>>() - .ok_or(SynthesisError::Unsatisfiable)?; - return Ok(Self(z)); - } - let cs = self.cs().or(other.cs()); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - - // Compute the result `z` outside the circuit and provide it as hints. - let z = { - let mut z = vec![(F::zero(), BigUint::zero()); len]; - for i in 0..self.0.len() { - for j in 0..other.0.len() { - z[i + j].0 += self.0[i].value().unwrap_or_default() - * other.0[j].value().unwrap_or_default(); - z[i + j].1 += &self.0[i].ub * &other.0[j].ub; - } - } - z.into_iter() - .map(|(v, ub)| { - assert!(ub < F::MODULUS_MINUS_ONE_DIV_TWO.into()); - Ok(LimbVar { - v: FpVar::new_variable(cs.clone(), || Ok(v), mode)?, - ub, - }) - }) - .collect::, _>>()? - }; - for c in 1..=len { - let c = F::from(c as u64); - let mut t = F::one(); - let mut c_powers = vec![]; - for _ in 0..len { - c_powers.push(t); - t *= c; - } - // `l = Σ self[i] c^i` - let l = self - .0 - .iter() - .zip(&c_powers) - .map(|(v, t)| &v.v * *t) - .sum::>(); - // `r = Σ other[i] c^i` - let r = other - .0 - .iter() - .zip(&c_powers) - .map(|(v, t)| &v.v * *t) - .sum::>(); - // `o = Σ z[i] c^i` - let o = z - .iter() - .zip(&c_powers) - .map(|(v, t)| &v.v * *t) - .sum::>(); - // Enforce `o = l * r` - l.mul_equals(&r, &o)?; - } - - Ok(Self(z)) - } - - /// Convert `Self` to an element in `M`, i.e., compute `Self % M::MODULUS`. - pub fn modulo(&self) -> Result { - let cs = self.cs(); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - let m: BigUint = M::MODULUS.into(); - // Provide the quotient and remainder as hints - let (q, r) = { - let v = self.value().unwrap_or_default(); - let (q, r) = v.div_rem(&m); - let q_ubound = self.ubound().div_ceil(&m); - let r_ubound = &m; - ( - Self::new_variable( - cs.clone(), - || Ok(BoundedBigUint(q, q_ubound.bits() as usize)), - mode, - )?, - Self::new_variable( - cs.clone(), - || Ok(BoundedBigUint(r, r_ubound.bits() as usize)), - mode, - )?, - ) - }; - - let m = Self::new_constant(cs.clone(), BoundedBigUint(m, M::MODULUS_BIT_SIZE as usize))?; - // Enforce `self = q * m + r` - q.mul_no_align(&m)? - .add_no_align(&r)? - .enforce_equal_unaligned(self)?; - // Enforce `r < m` (and `r >= 0` already holds) - r.enforce_lt(&m)?; - - Ok(r) - } - - /// Enforce that `self` is congruent to `other` modulo `M::MODULUS`. - pub fn enforce_congruent(&self, other: &Self) -> Result<(), SynthesisError> { - let cs = self.cs(); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - let m: BigUint = M::MODULUS.into(); - let bits = (max(self.ubound(), other.ubound()) / &m).bits() as usize; - // Provide the quotient `|x - y| / m` and a boolean indicating if `x > y` - // as hints. - let (q, is_ge) = { - let x = self.value().unwrap_or_default(); - let y = other.value().unwrap_or_default(); - let (d, b) = if x > y { - ((x - y) / &m, true) - } else { - ((y - x) / &m, false) - }; - ( - Self::new_variable(cs.clone(), || Ok(BoundedBigUint(d, bits)), mode)?, - Boolean::new_variable(cs.clone(), || Ok(b), mode)?, - ) - }; - - let zero = Self::new_constant(cs.clone(), BoundedBigUint(BigUint::zero(), bits))?; - let m = Self::new_constant(cs.clone(), BoundedBigUint(m, M::MODULUS_BIT_SIZE as usize))?; - let l = self.add_no_align(&is_ge.select(&zero, &q)?.mul_no_align(&m)?)?; - let r = other.add_no_align(&is_ge.select(&q, &zero)?.mul_no_align(&m)?)?; - // If `self >= other`, enforce `self = other + q * m` - // Otherwise, enforce `self + q * m = other` - // Soundness holds because if `self` and `other` are not congruent, then - // one can never find a `q` satisfying either equation above. - l.enforce_equal_unaligned(&r) - } -} - -impl EquivalenceGadget for NonNativeUintVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { - self.enforce_congruent::(other) - } -} - -impl]>> From for NonNativeUintVar { - fn from(bits: B) -> Self { - Self( - bits.as_ref() - .chunks(Self::bits_per_limb()) - .map(LimbVar::from) - .collect::>(), - ) - } -} - -impl AbsorbNonNativeGadget for NonNativeUintVar { - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { - let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; - - self.to_bits_le()? - .chunks(bits_per_limb) - .map(Boolean::le_bits_to_fp) - .collect() - } -} - -impl VectorGadget> for [NonNativeUintVar] { - fn add(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.add_no_align(y)) - .collect() - } - - fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.mul_no_align(y)) - .collect() - } - - fn scale( - &self, - other: &NonNativeUintVar, - ) -> Result>, SynthesisError> { - self.iter().map(|x| x.mul_no_align(other)).collect() - } -} - -impl MatrixGadget> for SparseMatrixVar> { - fn mul_vector( - &self, - v: &impl Index>, - ) -> Result>, SynthesisError> { - self.0 - .iter() - .map(|row| { - let len = row - .iter() - .map(|(value, col_i)| value.0.len() + v[*col_i].0.len() - 1) - .max() - .unwrap_or(0); - // This is a combination of `mul_no_align` and `add_no_align` - // that results in more flattened `LinearCombination`s. - // Consequently, `ConstraintSystem::inline_all_lcs` costs less - // time, thus making trusted setup and proof generation faster. - (0..len) - .map(|i| { - LimbVar::add_many( - &row.iter() - .flat_map(|(value, col_i)| { - let start = max(i + 1, v[*col_i].0.len()) - v[*col_i].0.len(); - let end = min(i + 1, value.0.len()); - (start..end).map(|j| value.0[j].mul(&v[*col_i].0[i - j])) - }) - .collect::>>()?, - ) - }) - .collect::>>() - .ok_or(SynthesisError::Unsatisfiable) - .map(NonNativeUintVar) - }) - .collect() - } -} - -#[cfg(test)] -mod tests { - use std::error::Error; - - use ark_ff::Field; - use ark_pallas::{Fq, Fr}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{test_rng, UniformRand}; - use num_bigint::RandBigInt; - - use super::*; - - #[test] - fn test_mul_biguint() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let size = 256; - - let rng = &mut test_rng(); - let a = rng.gen_biguint(size as u64); - let b = rng.gen_biguint(size as u64); - let ab = &a * &b; - let aab = &a * &ab; - let abb = &ab * &b; - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(a, size)))?; - let b_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(b, size)))?; - let ab_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(ab, size * 2)))?; - let aab_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(aab, size * 3)))?; - let abb_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok(BoundedBigUint(abb, size * 3)))?; - - a_var - .mul_no_align(&b_var)? - .enforce_equal_unaligned(&ab_var)?; - a_var - .mul_no_align(&ab_var)? - .enforce_equal_unaligned(&aab_var)?; - ab_var - .mul_no_align(&b_var)? - .enforce_equal_unaligned(&abb_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_mul_fq() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let rng = &mut test_rng(); - let a = Fq::rand(rng); - let b = Fq::rand(rng); - let ab = a * b; - let aab = a * ab; - let abb = ab * b; - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; - let b_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(b))?; - let ab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(ab))?; - let aab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(aab))?; - let abb_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(abb))?; - - a_var - .mul_no_align(&b_var)? - .enforce_congruent::(&ab_var)?; - a_var - .mul_no_align(&ab_var)? - .enforce_congruent::(&aab_var)?; - ab_var - .mul_no_align(&b_var)? - .enforce_congruent::(&abb_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_pow() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let rng = &mut test_rng(); - - let a = Fq::rand(rng); - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; - - let mut r_var = a_var.clone(); - for _ in 0..16 { - r_var = r_var.mul_no_align(&r_var)?.modulo::()?; - } - r_var = r_var.mul_no_align(&a_var)?.modulo::()?; - assert_eq!(a.pow([65537u64]), Fq::from(r_var.value()?)); - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_vec_vec_mul() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let len = 1000; - - let rng = &mut test_rng(); - let a = (0..len).map(|_| Fq::rand(rng)).collect::>(); - let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); - let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); - - let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; - let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; - let c_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(c))?; - - let mut r_var = - NonNativeUintVar::new_constant(cs.clone(), BoundedBigUint(BigUint::zero(), 0))?; - for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { - r_var = r_var.add_no_align(&a.mul_no_align(&b)?)?; - } - r_var.enforce_congruent::(&c_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index b6ced84d8..c91d06afb 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -1,7 +1,7 @@ +pub mod algebra; pub mod arithmetizations; pub mod circuits; pub mod commitments; -pub mod gadgets; pub mod relations; pub mod sumcheck; pub mod traits; diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index 717597cbc..141370f47 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -18,7 +18,7 @@ use ark_std::{cfg_chunks, cfg_into_iter, cfg_iter, fmt::Debug}; use rayon::prelude::*; use thiserror::Error; -use crate::{traits::Absorbable, transcripts::Transcript}; +use crate::transcripts::{Absorbable, Transcript}; use utils::{ barycentric_weights, compute_lagrange_interpolated_poly, extrapolate, VPAuxInfo, diff --git a/crates/primitives/src/traits.rs b/crates/primitives/src/traits.rs new file mode 100644 index 000000000..35d9be14e --- /dev/null +++ b/crates/primitives/src/traits.rs @@ -0,0 +1,56 @@ +pub use crate::algebra::{ + field::SonobeField, + group::{SonobeCurve, CF1, CF2}, +}; + +pub trait Dummy { + fn dummy(cfg: Cfg) -> Self; +} + +impl Dummy for Vec { + fn dummy(cfg: usize) -> Self { + vec![Default::default(); cfg] + } +} + +impl Dummy<()> for T { + fn dummy(_: ()) -> Self { + Default::default() + } +} + +/// Converts a value `self` into a vector of field elements, ordered in the same +/// way as how a variable of type `Var` would be represented *natively* in the +/// circuit. +/// +/// This is useful for the verifier to compute the public inputs. +pub trait Inputize { + fn inputize(&self) -> Vec; +} + +impl> Inputize for [T] { + fn inputize(&self) -> Vec { + self.iter().flat_map(Inputize::::inputize).collect() + } +} + +/// Converts a value `self` into a vector of field elements, ordered in the same +/// way as how a variable of type `Var` would be represented *non-natively* in +/// the circuit. +/// +/// This is useful for the verifier to compute the public inputs. +/// +/// Note that we require this trait because we need to distinguish between some +/// data types that are represented both natively and non-natively in-circuit +/// (e.g., field elements can have type `FpVar` and `NonNativeUintVar`). +pub trait InputizeNonNative { + fn inputize_nonnative(&self) -> Vec; +} + +impl> InputizeNonNative for [T] { + fn inputize_nonnative(&self) -> Vec { + self.iter() + .flat_map(InputizeNonNative::::inputize_nonnative) + .collect() + } +} diff --git a/crates/primitives/src/traits/mod.rs b/crates/primitives/src/traits/mod.rs deleted file mode 100644 index aecf72258..000000000 --- a/crates/primitives/src/traits/mod.rs +++ /dev/null @@ -1,345 +0,0 @@ -use ark_ec::{ - short_weierstrass::{Projective, SWCurveConfig}, - AffineRepr, CurveGroup, PrimeGroup, -}; -use ark_ff::{BigInteger, Field as ArkField, Fp, FpConfig, One, PrimeField, Zero}; -use ark_r1cs_std::{ - fields::{fp::FpVar, FieldVar}, - groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, -}; -use ark_relations::gr1cs::SynthesisError; -use ark_std::{ - any::TypeId, - iter::Sum, - mem::transmute_copy, - ops::{Add, Mul}, -}; - -pub type CF1 = ::ScalarField; -pub type CF2 = <::BaseField as ArkField>::BasePrimeField; - -pub trait Dummy { - fn dummy(cfg: Cfg) -> Self; -} - -impl Dummy for Vec { - fn dummy(cfg: usize) -> Self { - vec![Default::default(); cfg] - } -} - -impl Dummy<()> for T { - fn dummy(_: ()) -> Self { - Default::default() - } -} - -/// Converts a value `self` into a vector of field elements, ordered in the same -/// way as how a variable of type `Var` would be represented *natively* in the -/// circuit. -/// -/// This is useful for the verifier to compute the public inputs. -pub trait Inputize { - fn inputize(&self) -> Vec; -} - -/// Converts a value `self` into a vector of field elements, ordered in the same -/// way as how a variable of type `Var` would be represented *non-natively* in -/// the circuit. -/// -/// This is useful for the verifier to compute the public inputs. -/// -/// Note that we require this trait because we need to distinguish between some -/// data types that are represented both natively and non-natively in-circuit -/// (e.g., field elements can have type `FpVar` and `NonNativeUintVar`). -pub trait InputizeNonNative { - fn inputize_nonnative(&self) -> Vec; -} - -impl> Inputize for [T] { - fn inputize(&self) -> Vec { - self.iter().flat_map(Inputize::::inputize).collect() - } -} - -impl> InputizeNonNative for [T] { - fn inputize_nonnative(&self) -> Vec { - self.iter() - .flat_map(InputizeNonNative::::inputize_nonnative) - .collect() - } -} - -impl, const N: usize> Inputize for Fp { - /// Returns the internal representation in the same order as how the value - /// is allocated in `FpVar::new_input`. - fn inputize(&self) -> Vec { - vec![*self] - } -} - -impl> Inputize for Projective

{ - /// Returns the internal representation in the same order as how the value - /// is allocated in `ProjectiveVar::new_input`. - fn inputize(&self) -> Vec { - let affine = self.into_affine(); - match affine.xy() { - Some((x, y)) => vec![x, y, One::one()], - None => vec![Zero::zero(), One::one(), Zero::zero()], - } - } -} - -impl InputizeNonNative for P { - /// Returns the internal representation in the same order as how the value - /// is allocated in `NonNativeUintVar::new_input`. - fn inputize_nonnative(&self) -> Vec { - self.into_bigint() - .to_bits_le() - .chunks(F::BITS_PER_LIMB) - .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) - .collect() - } -} - -impl> - InputizeNonNative for Projective

-{ - /// Returns the internal representation in the same order as how the value - /// is allocated in `NonNativeAffineVar::new_input`. - fn inputize_nonnative(&self) -> Vec { - let affine = self.into_affine(); - let (x, y) = affine.xy().unwrap_or_default(); - - [x, y].inputize_nonnative() - } -} - -/// `Field` trait is a wrapper around `PrimeField` that also includes the -/// necessary bounds for the field to be used conveniently in folding schemes. -pub trait SonobeField: - PrimeField + Absorbable + Inputize -{ - const BITS_PER_LIMB: usize; - /// The in-circuit variable type for this field. - type Var: FieldVar; -} - -impl, const N: usize> SonobeField for Fp { - const BITS_PER_LIMB: usize = 55; // TODO: make this configurable - type Var = FpVar; -} - -/// `Curve` trait is a wrapper around `CurveGroup` that also includes the -/// necessary bounds for the curve to be used conveniently in folding schemes. -pub trait SonobeCurve: - CurveGroup - + Absorbable - + Inputize - + InputizeNonNative -{ - /// The in-circuit variable type for this curve. - type Var: CurveVar; -} - -impl> SonobeCurve - for Projective

-{ - type Var = ProjectiveVar>; -} - -pub trait Absorbable { - /// Converts the object into field elements that can be absorbed by a `CryptographicSponge`. - /// Append the list to `dest` - fn absorb_into(&self, dest: &mut Vec); - - /// Converts the object into field elements that can be absorbed by a `CryptographicSponge`. - /// Return the list as `Vec` - fn extract_absorbed(&self) -> Vec { - let mut result = Vec::new(); - self.absorb_into(&mut result); - result - } -} - -impl, const N: usize> Absorbable for Fp { - fn absorb_into(&self, dest: &mut Vec) { - if TypeId::of::() == TypeId::of::() { - // Safe because `F` and `Self` have the same type - // TODO (@winderica): specialization when??? - dest.push(unsafe { transmute_copy::(self) }); - } else { - let bits_per_limb = F::MODULUS_BIT_SIZE - 1; - let num_limbs = Self::MODULUS_BIT_SIZE.div_ceil(bits_per_limb); - - let mut limbs = self - .into_bigint() - .to_bits_le() - .chunks(bits_per_limb as usize) - .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) - .collect::>(); - limbs.resize(num_limbs as usize, F::zero()); - - dest.extend(&limbs) - } - } -} - -impl>> Absorbable for Projective

{ - fn absorb_into(&self, dest: &mut Vec) { - let affine = self.into_affine(); - let (x, y) = affine.xy().unwrap_or_default(); - [x, y].absorb_into(dest); - } -} - -impl Absorbable for usize { - fn absorb_into(&self, dest: &mut Vec) { - dest.push(F::from(*self as u64)); - } -} - -impl> Absorbable for &T { - fn absorb_into(&self, dest: &mut Vec) { - >::absorb_into(self, dest); - } -} - -impl> Absorbable for (T, T) { - fn absorb_into(&self, dest: &mut Vec) { - self.0.absorb_into(dest); - self.1.absorb_into(dest); - } -} - -impl + 'static> Absorbable for [T] { - fn absorb_into(&self, dest: &mut Vec) { - if TypeId::of::() == TypeId::of::() { - // Safe because `F` and `T` have the same type - dest.extend(unsafe { transmute_copy::<&[T], &[F]>(&self) }); - } else { - for t in self.iter() { - t.absorb_into(dest); - } - } - } -} - -impl + 'static, const N: usize> Absorbable for [T; N] { - fn absorb_into(&self, dest: &mut Vec) { - <[T] as Absorbable>::absorb_into(self, dest); - } -} - -impl + 'static> Absorbable for Vec { - fn absorb_into(&self, dest: &mut Vec) { - <[T] as Absorbable>::absorb_into(self, dest); - } -} - -// TODO: rework this -/// An interface for objects that can be absorbed by a `TranscriptVar` whose constraint field -/// is `F`. -/// -/// Matches `AbsorbGadget` in `ark-crypto-primitives`. -pub trait AbsorbNonNativeGadget { - /// Converts the object into field elements that can be absorbed by a `TranscriptVar`. - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError>; -} - -impl> AbsorbNonNativeGadget for &T { - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { - T::to_native_sponge_field_elements(self) - } -} - -impl> AbsorbNonNativeGadget for [T] { - fn to_native_sponge_field_elements(&self) -> Result>, SynthesisError> { - let mut result = Vec::new(); - for t in self.iter() { - result.extend(t.to_native_sponge_field_elements()?); - } - Ok(result) - } -} - -#[derive(Clone, Copy, Default, Debug)] -pub struct Null; - -impl Add for Null { - type Output = Null; - - fn add(self, _: F) -> Null { - Null - } -} - -impl Add for &Null { - type Output = Null; - - fn add(self, _: F) -> Null { - Null - } -} - -impl Mul for Null { - type Output = Self; - - fn mul(self, _: F) -> Null { - Null - } -} - -impl Mul for &Null { - type Output = Null; - - fn mul(self, _: F) -> Null { - Null - } -} - -impl Sum for Null { - fn sum>(_: I) -> Self { - Null - } -} - -pub trait ScalarRLC { - type Value; - - fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value; -} - -impl ScalarRLC for I -where - I::Item: Add + Sum + for<'a> Mul<&'a Coeff, Output = I::Item>, -{ - type Value = I::Item; - - fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value { - self.zip(coeffs).map(|(v, c)| v * c).sum::() - } -} - -pub trait SliceRLC { - type Value; - - fn slice_rlc(self, coeffs: &[Coeff]) -> Vec; -} - -impl<'a, T: 'a, I: Iterator, Coeff> SliceRLC for I -where - T: Add + Copy, - for<'x> T: Mul<&'x Coeff, Output = T>, -{ - type Value = T; - - fn slice_rlc(self, coeffs: &[Coeff]) -> Vec { - let mut iter = self.zip(coeffs).map(|(v, c)| v.iter().map(|x| *x * c)); - let first = iter.next().unwrap(); - - iter.fold(first.collect(), |acc, v| { - acc.into_iter().zip(v).map(|(a, b)| a + b).collect() - }) - } -} diff --git a/crates/primitives/src/transcripts/absorbable.rs b/crates/primitives/src/transcripts/absorbable.rs new file mode 100644 index 000000000..c19f7660e --- /dev/null +++ b/crates/primitives/src/transcripts/absorbable.rs @@ -0,0 +1,98 @@ +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::gr1cs::SynthesisError; +use ark_std::{any::TypeId, mem::transmute_copy}; + +pub trait Absorbable { + fn absorb_into(&self, dest: &mut Vec); + + fn to_absorbable(&self) -> Vec { + let mut result = Vec::new(); + self.absorb_into(&mut result); + result + } +} + +impl> Absorbable for usize { + fn absorb_into(&self, dest: &mut Vec) { + dest.push(F::from(*self as u64)); + } +} + +impl> Absorbable for &T { + fn absorb_into(&self, dest: &mut Vec) { + >::absorb_into(self, dest); + } +} + +impl> Absorbable for (T, T) { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest); + self.1.absorb_into(dest); + } +} + +impl> Absorbable for [T] { + fn absorb_into(&self, dest: &mut Vec) { + for t in self.iter() { + t.absorb_into(dest); + } + } +} + +impl, const N: usize> Absorbable for [T; N] { + fn absorb_into(&self, dest: &mut Vec) { + <[T] as Absorbable>::absorb_into(self, dest); + } +} + +impl> Absorbable for Vec { + fn absorb_into(&self, dest: &mut Vec) { + <[T] as Absorbable>::absorb_into(self, dest); + } +} + +/// An interface for objects that can be absorbed by a `TranscriptVar` whose constraint field +/// is `F`. +/// +/// Matches `AbsorbGadget` in `ark-crypto-primitives`. +pub trait AbsorbableGadget { + fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError>; + + fn to_absorbable(&self) -> Result, SynthesisError> { + let mut result = Vec::new(); + self.absorb_into(&mut result)?; + Ok(result) + } +} + +impl> AbsorbableGadget for &T { + fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { + >::absorb_into(self, dest) + } +} + +impl> AbsorbableGadget for (T, T) { + fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { + self.0.absorb_into(dest)?; + self.1.absorb_into(dest) + } +} + +impl> AbsorbableGadget for [T] { + fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { + self.iter().try_for_each(|t| t.absorb_into(dest)) + } +} + +impl, const N: usize> AbsorbableGadget for [T; N] { + fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { + <[T] as AbsorbableGadget>::absorb_into(self, dest) + } +} + +impl> AbsorbableGadget for Vec { + fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { + <[T] as AbsorbableGadget>::absorb_into(self, dest) + } +} diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index 504559a7e..21c709f19 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -4,10 +4,11 @@ use ark_crypto_primitives::sponge::{ use ark_ec::CurveGroup; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; -use ark_relations::gr1cs::SynthesisError; +use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; -use crate::traits::{AbsorbNonNativeGadget, Absorbable}; +pub use absorbable::{Absorbable, AbsorbableGadget}; +pub mod absorbable; pub mod poseidon; pub trait Transcript { @@ -25,14 +26,9 @@ pub trait Transcript { fn add + ?Sized>(&mut self, input: &A); - /// Squeeze `num_bytes` bytes from the sponge. - fn get_bytes(&mut self, num_bytes: usize) -> Vec; - /// Squeeze `num_bits` bits from the sponge. fn get_bits(&mut self, num_bits: usize) -> Vec; - fn get_field_elements_with_sizes(&mut self, sizes: &[FieldElementSize]) -> Vec; - fn get_field_elements(&mut self, num_elements: usize) -> Vec; /// Creates a new sponge with applied domain separation. @@ -49,7 +45,7 @@ pub trait Transcript { let limbs = input .chunks(F::MODULUS_BIT_SIZE.div_ceil(8) as usize) .map(|chunk| F::from_le_bytes_mod_order(chunk)) - .collect::>(); + .collect::>(); new_sponge.add(&limbs); @@ -82,49 +78,65 @@ pub trait Transcript { } } -pub trait TranscriptVar: - CryptographicSpongeVar -{ +pub trait TranscriptVar { + type Native; + /// `new_with_pp_hash` creates a new transcript / sponge with the given /// hash of the public parameters. fn new_with_pp_hash( config: &Self::Parameters, pp_hash: &FpVar, - ) -> Result; - - /// `absorb_point` is for absorbing points whose `BaseField` is the field of - /// the sponge, i.e., the type `C` of these points should satisfy - /// `C::BaseField = F`. - /// - /// If the sponge field `F` is `C::ScalarField`, call `absorb_nonnative` - /// instead. - fn absorb_point, GC: CurveVar>( - &mut self, - v: &GC, - ) -> Result<(), SynthesisError>; - /// `absorb_nonnative` is for structs that contain non-native (field or - /// group) elements, including: - /// - /// - A field element of type `T: PrimeField` that will be absorbed into a - /// sponge that operates in another field `F != T`. - /// - A group element of type `C: CurveGroup` that will be absorbed into a - /// sponge that operates in another field `F != C::BaseField`, e.g., - /// `F = C::ScalarField`. - /// - A `CommittedInstance` on the secondary curve (used for CycleFold) that - /// will be absorbed into a sponge that operates in the (scalar field of - /// the) primary curve. - /// - /// Note that although a `CommittedInstance` for `AugmentedFCircuit` on - /// the primary curve also contains non-native elements, we still regard - /// it as native, because the sponge is on the same curve. - fn absorb_nonnative>( - &mut self, - v: &V, - ) -> Result<(), SynthesisError>; - - fn get_challenge(&mut self) -> Result, SynthesisError>; - /// returns the bit representation of the challenge, we use its output in-circuit for the - /// `GC.scalar_mul_le` method. - fn get_challenge_nbits(&mut self, nbits: usize) -> Result>, SynthesisError>; - fn get_challenges(&mut self, n: usize) -> Result>, SynthesisError>; + ) -> Result + where + Self: CryptographicSpongeVar, + Self::Native: CryptographicSponge, + { + let mut sponge = Self::new(ConstraintSystemRef::None, config); + sponge.add(&pp_hash)?; + Ok(sponge) + } + + fn add>>(&mut self, input: &A) -> Result<(), SynthesisError>; + + /// Squeeze `num_bits` bits from the sponge. + fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError>; + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError>; + + /// Creates a new sponge with applied domain separation. + fn separate_domain(&self, domain: &[u8]) -> Result + where + Self: CryptographicSponge, + { + let mut new_sponge = self.clone(); + + let mut input = domain.len().to_le_bytes().to_vec(); + input.extend_from_slice(domain); + + let limbs = input + .chunks(F::MODULUS_BIT_SIZE.div_ceil(8) as usize) + .map(|chunk| FpVar::Constant(F::from_le_bytes_mod_order(chunk))) + .collect::>(); + + new_sponge.add(&limbs)?; + + Ok(new_sponge) + } + + fn challenge_field_element(&mut self) -> Result, SynthesisError> { + let mut c = self.get_field_elements(1)?; + self.add(&c[0])?; + Ok(c.pop().unwrap()) + } + fn challenge_bits(&mut self, nbits: usize) -> Result>, SynthesisError> { + let bits = self.get_bits(nbits)?; + self.add(&Boolean::le_bits_to_fp(&bits)?)?; + Ok(bits) + } + + fn challenge_field_elements(&mut self, n: usize) -> Result>, SynthesisError> { + let c = self.get_field_elements(n)?; + self.add(&c)?; + Ok(c) + } } diff --git a/crates/primitives/src/transcripts/poseidon.rs b/crates/primitives/src/transcripts/poseidon.rs index a3a298108..3607e95e7 100644 --- a/crates/primitives/src/transcripts/poseidon.rs +++ b/crates/primitives/src/transcripts/poseidon.rs @@ -14,7 +14,7 @@ use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; use crate::transcripts::{Absorbable, FieldElementSize}; -use super::{AbsorbNonNativeGadget, Transcript, TranscriptVar}; +use super::{AbsorbableGadget, Transcript, TranscriptVar}; impl Transcript for PoseidonSponge { fn add + ?Sized>(&mut self, input: &A) { @@ -33,7 +33,7 @@ impl Transcript for PoseidonSponge { dest.extend(unsafe { transmute_copy::<&[F], &[T]>(&self.0.as_ref()) }); } } - let v = input.extract_absorbed(); + let v = input.to_absorbable(); CryptographicSponge::absorb(self, &Hack(v)); } @@ -41,67 +41,24 @@ impl Transcript for PoseidonSponge { CryptographicSponge::squeeze_bits(self, num_bits) } - fn get_bytes(&mut self, num_bytes: usize) -> Vec { - CryptographicSponge::squeeze_bytes(self, num_bytes) - } - - fn get_field_elements_with_sizes(&mut self, sizes: &[FieldElementSize]) -> Vec { - self.squeeze_native_field_elements_with_sizes(sizes) - } - fn get_field_elements(&mut self, num_elements: usize) -> Vec { self.squeeze_native_field_elements(num_elements) } } -impl TranscriptVar> for PoseidonSpongeVar { - fn new_with_pp_hash( - config: &Self::Parameters, - pp_hash: &FpVar, - ) -> Result { - let mut sponge = Self::new(ConstraintSystemRef::None, config); - sponge.absorb(&pp_hash)?; - Ok(sponge) - } +impl TranscriptVar for PoseidonSpongeVar { + type Native = PoseidonSponge; - fn absorb_point, GC: CurveVar>( - &mut self, - v: &GC, - ) -> Result<(), SynthesisError> { - let mut vec = v.to_constraint_field()?; - // The last element in the vector tells whether the point is infinity, - // but we can in fact avoid absorbing it without loss of soundness. - // This is because the `to_constraint_field` method internally invokes - // [`ProjectiveVar::to_afine`](https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/groups/curves/short_weierstrass/mod.rs#L160-L195), - // which guarantees that an infinity point is represented as `(0, 0)`, - // but the y-coordinate of a non-infinity point is never 0 (for why, see - // https://crypto.stackexchange.com/a/108242 ). - vec.pop(); - self.absorb(&vec) - } - fn absorb_nonnative>( - &mut self, - v: &V, - ) -> Result<(), SynthesisError> { - self.absorb(&v.to_native_sponge_field_elements()?) - } - fn get_challenge(&mut self) -> Result, SynthesisError> { - let c = self.squeeze_field_elements(1)?; - self.absorb(&c[0])?; - Ok(c[0].clone()) + fn add>>(&mut self, input: &A) -> Result<(), SynthesisError> { + self.absorb(&input.to_absorbable()?) } - /// returns the bit representation of the challenge, we use its output in-circuit for the - /// `GC.scalar_mul_le` method. - fn get_challenge_nbits(&mut self, nbits: usize) -> Result>, SynthesisError> { - let bits = self.squeeze_bits(nbits)?; - self.absorb(&Boolean::le_bits_to_fp(&bits)?)?; - Ok(bits) + fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError> { + self.squeeze_bits(num_bits) } - fn get_challenges(&mut self, n: usize) -> Result>, SynthesisError> { - let c = self.squeeze_field_elements(n)?; - self.absorb(&c)?; - Ok(c) + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError> { + self.squeeze_field_elements(num_elements) } } @@ -149,8 +106,9 @@ pub mod tests { use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, test_rng}; + use crate::algebra::group::nonnative::NonNativeAffineVar; + use super::*; - use crate::gadgets::nonnative::affine::NonNativeAffineVar; // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 #[test] @@ -163,8 +121,8 @@ pub mod tests { .into_iter() .map(Fr::from) .collect::>(); - poseidon_sponge.absorb(&v); - poseidon_sponge.squeeze_field_elements::(1); + poseidon_sponge.add(&v); + poseidon_sponge.get_field_elements(1); assert!( poseidon_sponge.state[0] == Fr::from_str( @@ -193,8 +151,8 @@ pub mod tests { ConstraintSystem::::new_ref(), || Ok(p), )?; - tr_var.absorb_point(&p_var)?; - let c_var = tr_var.get_challenge()?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; // assert that native & gadget transcripts return the same challenge assert_eq!(c, c_var.value()?); @@ -217,8 +175,8 @@ pub mod tests { let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); let p_var = NonNativeAffineVar::::new_witness(ConstraintSystem::::new_ref(), || Ok(p))?; - tr_var.absorb_nonnative(&p_var)?; - let c_var = tr_var.get_challenge()?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; // assert that native & gadget transcripts return the same challenge assert_eq!(c, c_var.value()?); @@ -237,8 +195,8 @@ pub mod tests { let cs = ConstraintSystem::::new_ref(); let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; - tr_var.absorb(&v)?; - let c_var = tr_var.get_challenge()?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_field_element()?; // assert that native & gadget transcripts return the same challenge assert_eq!(c, c_var.value()?); @@ -261,10 +219,10 @@ pub mod tests { let cs = ConstraintSystem::::new_ref(); let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; - tr_var.absorb(&v)?; + tr_var.add(&v)?; // get challenge from circuit transcript - let c_var = tr_var.get_challenge_nbits(nbits)?; + let c_var = tr_var.challenge_bits(nbits)?; let p = G1::generator(); let p_var = GVar::new_witness(cs.clone(), || Ok(p))?; From bded87265864afe7619f9eae06811a584c55f288 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 24 Oct 2025 22:09:08 +0800 Subject: [PATCH 05/93] Remove referenceable --- .../src/arithmetizations/ccs/mod.rs | 8 +-- crates/primitives/src/arithmetizations/mod.rs | 12 ++-- .../src/arithmetizations/r1cs/mod.rs | 61 ++++++++----------- crates/primitives/src/relations/mod.rs | 20 +----- 4 files changed, 38 insertions(+), 63 deletions(-) diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 9fa101602..ac97cb9f7 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -135,14 +135,14 @@ impl Arith for CCS { } } -impl ArithRelation, Vec> for CCS { +impl, U: AsRef<[F]>> ArithRelation for CCS { type Evaluation = Vec; - fn eval_relation(&self, w: &[F], u: &[F]) -> Result { - self.eval_assignments((F::one(), u, w).into()) + fn eval_relation(&self, w: &W, u: &U) -> Result { + self.eval_assignments((F::one(), u.as_ref(), w.as_ref()).into()) } - fn check_evaluation(_w: &[F], _u: &[F], e: Self::Evaluation) -> Result<(), Error> { + fn check_evaluation(_w: &W, _u: &U, e: Self::Evaluation) -> Result<(), Error> { cfg_into_iter!(e) .all(|i| i.is_zero()) .then_some(()) diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index 5567ef644..21cdc5cca 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,7 +1,7 @@ use ark_relations::gr1cs::SynthesisError; use thiserror::Error; -use crate::relations::{Referenceable, Relation}; +use crate::relations::{Relation}; pub mod ccs; pub mod r1cs; @@ -71,7 +71,7 @@ pub trait Arith: Clone { /// This is also the case of CCS, where `W` and `U` may be vectors of field /// elements, [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::lcccs::LCCCS`], /// or [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::cccs::CCCS`]. -pub trait ArithRelation: Arith { +pub trait ArithRelation: Arith { type Evaluation; /// Evaluates the constraint system `self` at witness `w` and instance `u`. @@ -90,7 +90,7 @@ pub trait ArithRelation: Arith { /// /// However, we use `Self::Evaluation` to represent the evaluation result /// for future extensibility. - fn eval_relation(&self, w: W::Ref<'_>, u: U::Ref<'_>) -> Result; + fn eval_relation(&self, w: &W, u: &U) -> Result; /// Checks if the evaluation result is valid. The witness `w` and instance /// `u` are also parameters, because the validity check may need information @@ -106,10 +106,10 @@ pub trait ArithRelation: Arith { /// - The evaluation `v` of relaxed R1CS in ProtoGalaxy at satisfying `W` /// and `U` should satisfy `e = Σ pow_i(β) v_i`, where `e` is the error /// term in the committed instance. - fn check_evaluation(w: W::Ref<'_>, u: U::Ref<'_>, v: Self::Evaluation) -> Result<(), Error>; + fn check_evaluation(w: &W, u: &U, v: Self::Evaluation) -> Result<(), Error>; } -impl> Relation for A { +impl> Relation for A { type Error = Error; /// Checks if witness `w` and instance `u` satisfy the constraint system @@ -117,7 +117,7 @@ impl> Relation /// validity of the evaluation result. /// /// Used only for testing. - fn check_relation(&self, w: W::Ref<'_>, u: U::Ref<'_>) -> Result<(), Self::Error> { + fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error> { let e = self.eval_relation(w, u)?; Self::check_evaluation(w, u, e) } diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 76eae96ce..9e39b7585 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -1,13 +1,15 @@ +use std::ops::Index; use ark_ff::Field; use ark_relations::gr1cs::{ConstraintSystem, Matrix}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{cfg_into_iter, cfg_iter}; +use ark_std::iterable::Iterable; #[cfg(feature = "parallel")] use rayon::prelude::*; use crate::{ circuits::{Assignments, ConstraintSystemExt}, - relations::{Referenceable, WitnessInstanceExtractor}, + relations::{WitnessInstanceExtractor}, traits::Dummy, }; @@ -142,14 +144,14 @@ impl TryFrom> for R1CS { } } -impl ArithRelation, Vec> for R1CS { +impl, U: AsRef<[F]>> ArithRelation for R1CS { type Evaluation = Vec; - fn eval_relation(&self, w: &[F], x: &[F]) -> Result { + fn eval_relation(&self, w: &W, x: &U) -> Result { self.eval_assignments((F::one(), x.as_ref(), w.as_ref()).into()) } - fn check_evaluation(_w: &[F], _x: &[F], e: Self::Evaluation) -> Result<(), Error> { + fn check_evaluation(_w: &W, _x: &U, e: Self::Evaluation) -> Result<(), Error> { cfg_into_iter!(e) .all(|i| i.is_zero()) .then_some(()) @@ -168,49 +170,33 @@ impl WitnessInstanceExtractor, Vec> for R1CS { } } -pub struct RelaxedWitness { - pub w: Vec, - pub e: Vec, +pub struct RelaxedWitness { + pub w: V, + pub e: V, } -impl Referenceable for RelaxedWitness { - type Ref<'a> = (&'a [F], &'a [F]); - - fn reference(&self) -> Self::Ref<'_> { - (&self.w, &self.e) - } +pub struct RelaxedInstance { + pub x: V, + pub u: V::Item, } -pub struct RelaxedInstance { - pub x: Vec, - pub u: F, -} - -impl Referenceable for RelaxedInstance { - type Ref<'a> = (&'a [F], F); - - fn reference(&self) -> Self::Ref<'_> { - (&self.x, self.u) - } -} - -impl ArithRelation, RelaxedInstance> for R1CS { +impl ArithRelation, RelaxedInstance<&[F]>> for R1CS { type Evaluation = Vec; fn eval_relation( &self, - (w, _e): (&[F], &[F]), - (x, u): (&[F], F), + w: &RelaxedWitness<&[F]>, + u: &RelaxedInstance<&[F]>, ) -> Result { - self.eval_assignments((u, x, w).into()) + self.eval_assignments((*u.u, u.x, w.w).into()) } fn check_evaluation( - (_w, e): (&[F], &[F]), - _: (&[F], F), + w: &RelaxedWitness<&[F]>, + _u: &RelaxedInstance<&[F]>, v: Self::Evaluation, ) -> Result<(), Error> { - cfg_iter!(e) + cfg_iter!(w.e) .zip(&v) .all(|(e, v)| e == v) .then_some(()) @@ -220,11 +206,16 @@ impl ArithRelation, RelaxedInstance> for R1CS } } -impl WitnessInstanceExtractor, RelaxedInstance> for R1CS { +impl WitnessInstanceExtractor>, RelaxedInstance>> + for R1CS +{ type Source = Assignments>; type Error = Error; - fn extract(&self, z: Self::Source) -> Result<(RelaxedWitness, RelaxedInstance), Error> { + fn extract( + &self, + z: Self::Source, + ) -> Result<(RelaxedWitness>, RelaxedInstance>), Error> { let (w, x) = self.extract(z)?; let e = vec![F::zero(); self.n_constraints()]; Ok((RelaxedWitness { w, e }, RelaxedInstance { x, u: F::one() })) diff --git a/crates/primitives/src/relations/mod.rs b/crates/primitives/src/relations/mod.rs index c9349c2dd..6c3241700 100644 --- a/crates/primitives/src/relations/mod.rs +++ b/crates/primitives/src/relations/mod.rs @@ -2,27 +2,11 @@ use ark_std::{error::Error, rand::RngCore}; use crate::traits::Dummy; -pub trait Referenceable { - type Ref<'a>: Copy - where - Self: 'a; - - fn reference(&self) -> Self::Ref<'_>; -} - -impl Referenceable for Vec { - type Ref<'a> = &'a [T]; - - fn reference(&self) -> Self::Ref<'_> { - self - } -} - -pub trait Relation { +pub trait Relation { type Error: Error; /// Checks if witness `w` and instance `u` satisfy the relation `self` - fn check_relation(&self, w: W::Ref<'_>, u: U::Ref<'_>) -> Result<(), Self::Error>; + fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error>; } pub trait WitnessInstanceExtractor { From 7df4760b7cb3bce225a2a52b8debc372b4f98841 Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 25 Oct 2025 23:27:13 +0800 Subject: [PATCH 06/93] Cleanup --- crates/primitives/src/algebra/field/mod.rs | 2 +- .../primitives/src/algebra/field/nonnative.rs | 4 +- .../src/algebra/field/nonnative2.rs | 59 ++++++++++++++----- crates/primitives/src/algebra/group/mod.rs | 20 +++---- .../primitives/src/algebra/group/nonnative.rs | 11 ++-- crates/primitives/src/algebra/ops/matrix.rs | 4 +- crates/primitives/src/arithmetizations/mod.rs | 2 +- .../src/arithmetizations/r1cs/mod.rs | 6 +- crates/primitives/src/commitments/mod.rs | 28 +++++++-- crates/primitives/src/commitments/pedersen.rs | 11 ++-- .../primitives/src/transcripts/absorbable.rs | 3 - crates/primitives/src/transcripts/poseidon.rs | 23 ++++---- 12 files changed, 104 insertions(+), 69 deletions(-) diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs index 4f836b807..ce2d734f4 100644 --- a/crates/primitives/src/algebra/field/mod.rs +++ b/crates/primitives/src/algebra/field/mod.rs @@ -8,7 +8,7 @@ use crate::{ transcripts::{Absorbable, AbsorbableGadget}, }; -pub mod nonnative; +// pub mod nonnative; pub mod nonnative2; /// `Field` trait is a wrapper around `PrimeField` that also includes the diff --git a/crates/primitives/src/algebra/field/nonnative.rs b/crates/primitives/src/algebra/field/nonnative.rs index ef051fef1..006d8fd0f 100644 --- a/crates/primitives/src/algebra/field/nonnative.rs +++ b/crates/primitives/src/algebra/field/nonnative.rs @@ -1101,12 +1101,10 @@ impl MatrixGadget> for SparseMatrixVar { _cfg: PhantomData, - limbs: Vec>, - bounds: Vec, + pub limbs: Vec>, + pub bounds: Vec, } pub type BigIntVar = IntVarInner; @@ -542,10 +542,40 @@ impl TryFrom { -// Aligned(UintVarInner), -// Unaligned(UintVarInner), -// } +impl EqGadget for IntVarInner { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { + let mut result = Boolean::TRUE; + if self.limbs.len() != other.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.bounds.len() != other.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + for i in 0..self.limbs.len() { + if self.bounds[i] != other.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + result &= self.limbs[i].is_eq(&other.limbs[i])?; + } + Ok(result) + } + + fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { + if self.limbs.len() != other.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.bounds.len() != other.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + for i in 0..self.limbs.len() { + if self.bounds[i] != other.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + self.limbs[i].enforce_equal(&other.limbs[i])?; + } + Ok(()) + } +} impl FromBitsGadget for IntVarInner { fn from_bits_le(bits: &[Boolean]) -> Result { @@ -585,8 +615,9 @@ impl AbsorbableGadget> for IntVarInner VectorGadget> for [IntVarInner] { +impl VectorGadget> + for [IntVarInner] +{ fn add(&self, other: &Self) -> Result>, SynthesisError> { self.iter() .zip(other.iter()) @@ -609,7 +640,9 @@ impl VectorGadget> for [IntVarIn } } -impl MatrixGadget> for SparseMatrixVar> { +impl MatrixGadget> + for SparseMatrixVar> +{ fn mul_vector( &self, v: &impl Index>, @@ -639,7 +672,7 @@ impl MatrixGadget> for SparseM }) .collect::>(), ) - .filter_safe::() + .filter_safe::() }) .collect::>>() .ok_or(SynthesisError::Unsatisfiable)?; @@ -947,12 +980,10 @@ impl_assignment_op!( #[cfg(test)] mod tests { - use std::error::Error; - use ark_ff::Field; use ark_pallas::{Fq, Fr}; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{test_rng, UniformRand}; + use ark_std::{error::Error, test_rng, UniformRand}; use num_bigint::RandBigInt; use super::*; diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs index 1f04e1472..3f4b0fb34 100644 --- a/crates/primitives/src/algebra/group/mod.rs +++ b/crates/primitives/src/algebra/group/mod.rs @@ -14,15 +14,11 @@ use ark_r1cs_std::{ }; use ark_relations::gr1cs::SynthesisError; use ark_std::mem::swap; -use num_bigint::{BigInt, BigUint, Sign}; +use num_bigint::{BigInt, Sign}; use num_integer::Integer; -use crate::algebra::field::nonnative2::IntVarInner; use crate::{ - algebra::field::{ - nonnative::{Bound, NonNativeUintVar}, - SonobeField, - }, + algebra::field::SonobeField, traits::{Inputize, InputizeNonNative}, transcripts::{Absorbable, AbsorbableGadget}, }; @@ -162,12 +158,12 @@ impl PointScalarMulGadget> for C { let b_is_negative = Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(b_sign == Sign::Minus))?; - let a = NonNativeUintVar::new_variable_with_inferred_mode(cs.clone(), || { - Ok((a_abs.into(), Bound::new_ub(m_sqrt.clone()))) - })?; - let b = NonNativeUintVar::new_variable_with_inferred_mode(cs, || { - Ok((b_abs.into(), Bound::new_ub(m_sqrt))) - })?; + // let a = NonNativeUintVar::new_variable_with_inferred_mode(cs.clone(), || { + // Ok((a_abs.into(), Bound::new_ub(m_sqrt.clone()))) + // })?; + // let b = NonNativeUintVar::new_variable_with_inferred_mode(cs, || { + // Ok((b_abs.into(), Bound::new_ub(m_sqrt))) + // })?; todo!() } diff --git a/crates/primitives/src/algebra/group/nonnative.rs b/crates/primitives/src/algebra/group/nonnative.rs index 4663ba43d..fd4a11bd3 100644 --- a/crates/primitives/src/algebra/group/nonnative.rs +++ b/crates/primitives/src/algebra/group/nonnative.rs @@ -12,17 +12,18 @@ use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; use ark_std::borrow::Borrow; use crate::{ - algebra::{field::nonnative::NonNativeUintVar, group::SonobeCurve}, + algebra::{group::SonobeCurve}, transcripts::AbsorbableGadget, }; +use crate::algebra::field::nonnative2::BigIntVar; /// NonNativeAffineVar represents an elliptic curve point in Affine representation in the non-native /// field, over the constraint field. It is not intended to perform operations, but just to contain /// the affine coordinates in order to perform hash operations of the point. #[derive(Debug, Clone)] pub struct NonNativeAffineVar { - pub x: NonNativeUintVar, - pub y: NonNativeUintVar, + pub x: BigIntVar, + pub y: BigIntVar, } impl AllocVar for NonNativeAffineVar { @@ -37,8 +38,8 @@ impl AllocVar for NonNativeAffineVar { let affine = val.borrow().into_affine(); let (x, y) = affine.xy().unwrap_or_default(); - let x = NonNativeUintVar::new_variable(cs.clone(), || Ok(x), mode)?; - let y = NonNativeUintVar::new_variable(cs.clone(), || Ok(y), mode)?; + let x = BigIntVar::new_variable(cs.clone(), || Ok(x), mode)?; + let y = BigIntVar::new_variable(cs.clone(), || Ok(y), mode)?; Ok(Self { x, y }) }) diff --git a/crates/primitives/src/algebra/ops/matrix.rs b/crates/primitives/src/algebra/ops/matrix.rs index bc9d524f3..38ed5d44c 100644 --- a/crates/primitives/src/algebra/ops/matrix.rs +++ b/crates/primitives/src/algebra/ops/matrix.rs @@ -5,9 +5,7 @@ use ark_r1cs_std::{ GR1CSVar, }; use ark_relations::gr1cs::{Matrix, Namespace, SynthesisError}; -use ark_std::borrow::Borrow; - -use std::ops::Index; +use ark_std::{borrow::Borrow, ops::Index}; pub trait MatrixGadget { fn mul_vector(&self, v: &impl Index) -> Result, SynthesisError>; diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index 21cdc5cca..01a51d8a8 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,7 +1,7 @@ use ark_relations::gr1cs::SynthesisError; use thiserror::Error; -use crate::relations::{Relation}; +use crate::relations::Relation; pub mod ccs; pub mod r1cs; diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 9e39b7585..00a75740c 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -1,15 +1,13 @@ -use std::ops::Index; use ark_ff::Field; use ark_relations::gr1cs::{ConstraintSystem, Matrix}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{cfg_into_iter, cfg_iter}; -use ark_std::iterable::Iterable; +use ark_std::{cfg_into_iter, cfg_iter, iterable::Iterable}; #[cfg(feature = "parallel")] use rayon::prelude::*; use crate::{ circuits::{Assignments, ConstraintSystemExt}, - relations::{WitnessInstanceExtractor}, + relations::WitnessInstanceExtractor, traits::Dummy, }; diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index 18da7f641..a8b1a9b13 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,7 +1,8 @@ use ark_ff::Field; -use ark_r1cs_std::alloc::AllocVar; -use ark_relations::gr1cs::SynthesisError; +use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::{ + borrow::Borrow, fmt::Debug, iter::Sum, ops::{Add, Mul}, @@ -15,7 +16,8 @@ pub mod pedersen; #[derive(Debug, Error)] pub enum Error { // Commitment errors - #[error("The message being committed to has length {1}, exceeding the maximum supported length of {0}")] + #[error("The message being committed to has length {1}, exceeding the maximum supported length of {0}" + )] MessageTooLong(usize, usize), #[error("Blinding factor not 0 for Commitment without hiding")] BlindingNotZero, @@ -62,9 +64,11 @@ pub trait VectorCommitment: 'static + Debug + PartialEq { pub trait VectorCommitmentGadget { type Native: VectorCommitment; + type ConstraintField: Field; type KeyVar; type ScalarVar: Clone + + AllocVar<::Scalar, Self::ConstraintField> + Add + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> + Mul @@ -79,8 +83,12 @@ pub trait VectorCommitmentGadget { + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> + Mul + for<'a> Mul<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar>; - type CommitmentVar: Clone; - type RandomnessVar; + type CommitmentVar: Clone + + AllocVar<::Commitment, Self::ConstraintField>; + type RandomnessVar: AllocVar< + ::Randomness, + Self::ConstraintField, + >; fn open( ck: &Self::KeyVar, @@ -131,6 +139,16 @@ impl Sum for Null { } } +impl AllocVar for Null { + fn new_variable>( + _cs: impl Into>, + _f: impl FnOnce() -> Result, + _mode: AllocationMode, + ) -> Result { + Ok(Self) + } +} + #[cfg(test)] mod tests { use ark_ff::UniformRand; diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 1716a1d53..bac75c6e8 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -18,13 +18,12 @@ use ark_relations::gr1cs::SynthesisError; use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; use super::{Error, VectorCommitment}; +use crate::traits::CF1; use crate::{ - algebra::field::{nonnative::NonNativeUintVar, nonnative2::IntVarInner}, + algebra::field::nonnative2::NonNativeFieldVar, commitments::{Null, VectorCommitmentGadget}, - traits::{CF2, SonobeCurve}, + traits::{SonobeCurve, CF2}, }; -use crate::algebra::field::nonnative2::NonNativeFieldVar; -use crate::traits::CF1; #[derive(Debug, PartialEq)] pub struct Pedersen { @@ -225,6 +224,7 @@ impl PedersenGadget { impl VectorCommitmentGadget for PedersenGadget { type Native = Pedersen; + type ConstraintField = CF2; type KeyVar = Vec; @@ -234,7 +234,7 @@ impl VectorCommitmentGadget for PedersenGadget { type CommitmentVar = C::Var; - type RandomnessVar = (); + type RandomnessVar = Null; fn open( ck: &Self::KeyVar, @@ -254,6 +254,7 @@ impl VectorCommitmentGadget for PedersenGadget { impl VectorCommitmentGadget for PedersenGadget { type Native = Pedersen; + type ConstraintField = CF2; type KeyVar = (Vec, C::Var); diff --git a/crates/primitives/src/transcripts/absorbable.rs b/crates/primitives/src/transcripts/absorbable.rs index c19f7660e..31c80dea7 100644 --- a/crates/primitives/src/transcripts/absorbable.rs +++ b/crates/primitives/src/transcripts/absorbable.rs @@ -1,7 +1,4 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::fields::fp::FpVar; use ark_relations::gr1cs::SynthesisError; -use ark_std::{any::TypeId, mem::transmute_copy}; pub trait Absorbable { fn absorb_into(&self, dest: &mut Vec); diff --git a/crates/primitives/src/transcripts/poseidon.rs b/crates/primitives/src/transcripts/poseidon.rs index 3607e95e7..1b10ef38a 100644 --- a/crates/primitives/src/transcripts/poseidon.rs +++ b/crates/primitives/src/transcripts/poseidon.rs @@ -1,5 +1,3 @@ -use std::mem::transmute_copy; - use ark_crypto_primitives::sponge::{ constraints::CryptographicSpongeVar, poseidon::{ @@ -10,9 +8,10 @@ use ark_crypto_primitives::sponge::{ use ark_ec::CurveGroup; use ark_ff::PrimeField; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::mem::transmute_copy; -use crate::transcripts::{Absorbable, FieldElementSize}; +use crate::transcripts::Absorbable; use super::{AbsorbableGadget, Transcript, TranscriptVar}; @@ -104,7 +103,7 @@ pub mod tests { alloc::AllocVar, groups::curves::short_weierstrass::ProjectiveVar, GR1CSVar, }; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{error::Error, test_rng}; + use ark_std::{error::Error, str::FromStr, test_rng}; use crate::algebra::group::nonnative::NonNativeAffineVar; @@ -113,8 +112,6 @@ pub mod tests { // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 #[test] fn check_against_circom_poseidon() -> Result<(), Box> { - use std::str::FromStr; - let config = poseidon_canonical_config::(); let mut poseidon_sponge: PoseidonSponge<_> = CryptographicSponge::new(&config); let v = vec![1, 2, 3, 4] @@ -123,12 +120,12 @@ pub mod tests { .collect::>(); poseidon_sponge.add(&v); poseidon_sponge.get_field_elements(1); - assert!( - poseidon_sponge.state[0] - == Fr::from_str( - "18821383157269793795438455681495246036402687001665670618754263018637548127333" - ) - .unwrap() + assert_eq!( + poseidon_sponge.state[0], + Fr::from_str( + "18821383157269793795438455681495246036402687001665670618754263018637548127333" + ) + .unwrap() ); Ok(()) } From 3101c7fde992d684bde58b7ef51352f736c4b3ea Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 27 Oct 2025 17:19:35 +0800 Subject: [PATCH 07/93] Better design & convenient traits and utils --- crates/primitives/Cargo.toml | 1 + .../field/{nonnative2.rs => emulated.rs} | 229 ++- crates/primitives/src/algebra/field/mod.rs | 26 +- .../primitives/src/algebra/field/nonnative.rs | 1232 ----------------- .../group/{nonnative.rs => emulated.rs} | 79 +- crates/primitives/src/algebra/group/mod.rs | 123 +- crates/primitives/src/algebra/mod.rs | 15 + crates/primitives/src/algebra/ops/bits.rs | 6 +- crates/primitives/src/algebra/ops/mod.rs | 1 + crates/primitives/src/algebra/ops/pow.rs | 30 + crates/primitives/src/algebra/ops/rlc.rs | 8 +- .../src/arithmetizations/ccs/circuits.rs | 10 +- .../src/arithmetizations/ccs/mod.rs | 209 ++- crates/primitives/src/arithmetizations/mod.rs | 50 +- .../src/arithmetizations/r1cs/circuits.rs | 5 +- .../src/arithmetizations/r1cs/mod.rs | 236 ++-- crates/primitives/src/circuits/mod.rs | 21 +- crates/primitives/src/circuits/utils.rs | 69 +- crates/primitives/src/commitments/mod.rs | 123 +- crates/primitives/src/commitments/pedersen.rs | 107 +- crates/primitives/src/lib.rs | 1 + crates/primitives/src/sumcheck/circuits.rs | 117 ++ crates/primitives/src/sumcheck/mod.rs | 146 +- crates/primitives/src/sumcheck/utils.rs | 104 +- crates/primitives/src/traits.rs | 22 +- .../primitives/src/transcripts/absorbable.rs | 70 +- .../primitives/src/transcripts/griffin/mod.rs | 567 ++++++++ .../src/transcripts/griffin/sponge.rs | 471 +++++++ crates/primitives/src/transcripts/mod.rs | 99 +- .../src/transcripts/poseidon/mod.rs | 37 + .../{poseidon.rs => poseidon/sponge.rs} | 110 +- crates/primitives/src/utils/mod.rs | 2 + crates/primitives/src/utils/null.rs | 75 + crates/primitives/src/utils/vec.rs | 109 ++ 34 files changed, 2535 insertions(+), 1975 deletions(-) rename crates/primitives/src/algebra/field/{nonnative2.rs => emulated.rs} (85%) delete mode 100644 crates/primitives/src/algebra/field/nonnative.rs rename crates/primitives/src/algebra/group/{nonnative.rs => emulated.rs} (65%) create mode 100644 crates/primitives/src/algebra/ops/pow.rs create mode 100644 crates/primitives/src/sumcheck/circuits.rs create mode 100644 crates/primitives/src/transcripts/griffin/mod.rs create mode 100644 crates/primitives/src/transcripts/griffin/sponge.rs create mode 100644 crates/primitives/src/transcripts/poseidon/mod.rs rename crates/primitives/src/transcripts/{poseidon.rs => poseidon/sponge.rs} (71%) create mode 100644 crates/primitives/src/utils/mod.rs create mode 100644 crates/primitives/src/utils/null.rs create mode 100644 crates/primitives/src/utils/vec.rs diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 7bdb8fc6f..bf252b6a2 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -19,6 +19,7 @@ num-bigint = { workspace = true, features = ["rand"] } num-integer = { workspace = true } num-traits = { workspace = true } rayon = { workspace = true } +sha3 = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/primitives/src/algebra/field/nonnative2.rs b/crates/primitives/src/algebra/field/emulated.rs similarity index 85% rename from crates/primitives/src/algebra/field/nonnative2.rs rename to crates/primitives/src/algebra/field/emulated.rs index b854d1daf..73a5ec9cc 100644 --- a/crates/primitives/src/algebra/field/nonnative2.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -12,6 +12,7 @@ use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{ borrow::Borrow, cmp::{max, min}, + fmt::Debug, marker::PhantomData, ops::Index, }; @@ -19,12 +20,11 @@ use num_bigint::{BigInt, BigUint, Sign}; use num_integer::Integer; use num_traits::Signed; -use crate::algebra::ops::bits::{FromBitsGadget, ToBitsGadgetExt}; use crate::{ algebra::{ field::SonobeField, ops::{ - eq::EquivalenceGadget, + bits::{FromBitsGadget, ToBitsGadgetExt}, matrix::{MatrixGadget, SparseMatrixVar}, vector::VectorGadget, }, @@ -79,6 +79,20 @@ impl Bound { } } +fn compose>(limbs: V) -> BigInt { + let mut r = BigInt::zero(); + + for &limb in limbs.borrow().iter().rev() { + r <<= F::BITS_PER_LIMB; + r += if limb.into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO { + BigInt::from_biguint(Sign::Minus, (-limb).into()) + } else { + BigInt::from_biguint(Sign::Plus, limb.into()) + }; + } + r +} + #[derive(Debug, Clone)] pub struct IntVarInner { _cfg: PhantomData, @@ -86,11 +100,10 @@ pub struct IntVarInner { pub bounds: Vec, } -pub type BigIntVar = IntVarInner; -pub type NonNativeFieldVar = - IntVarInner; +pub type BigIntVar = IntVarInner; +pub type EmulatedFieldVar = IntVarInner; -impl GR1CSVar for IntVarInner { +impl GR1CSVar for IntVarInner { type Value = BigInt; fn cs(&self) -> ConstraintSystemRef { @@ -98,23 +111,33 @@ impl GR1CSVar for IntVarInner Result { - let mut r = BigInt::zero(); + self.limbs.value().map(compose) + } +} - for limb in self.limbs.value()?.into_iter().rev() { - r <<= F::BITS_PER_LIMB; - r += if limb.into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO { - BigInt::from_biguint(Sign::Minus, (-limb).into()) - } else { - BigInt::from_biguint(Sign::Plus, limb.into()) - }; - } +impl GR1CSVar + for IntVarInner +{ + type Value = Target; - Ok(r) + fn cs(&self) -> ConstraintSystemRef { + self.limbs.cs() + } + + fn value(&self) -> Result { + self.limbs.value().map(compose).map(|v| { + let (sign, abs) = v.into_parts(); + assert!(abs < Target::MODULUS.into()); + match sign { + Sign::Plus | Sign::NoSign => Target::from(abs), + Sign::Minus => Target::zero() - Target::from(abs), + } + }) } } impl IntVarInner { - fn new(limbs: Vec>, bounds: Vec) -> Self { + pub fn new(limbs: Vec>, bounds: Vec) -> Self { Self { _cfg: PhantomData, limbs, @@ -483,7 +506,7 @@ impl IntVarInner +impl IntVarInner { /// Convert `Self` to an element in `M`, i.e., compute `Self % M::MODULUS`. @@ -493,10 +516,16 @@ impl // Provide the quotient and remainder as hints let q = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); - Ok((self.value()?.div_floor(&m), Bound(lb, ub))) + Ok(( + compose(self.limbs.value().unwrap_or_default()).div_floor(&m), + Bound(lb, ub), + )) })?; let r = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { - Ok((self.value()?.abs() % &m, Bound(Zero::zero(), m.clone()))) + Ok(( + compose(self.limbs.value().unwrap_or_default()).abs() % &m, + Bound(Zero::zero(), m.clone()), + )) })?; let m = IntVarInner::constant(m); @@ -521,7 +550,10 @@ impl // Provide the quotient as hint let q = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); - Ok((self.value()?.div_floor(&m), Bound(lb, ub))) + Ok(( + compose(self.limbs.value().unwrap_or_default()).div_floor(&m), + Bound(lb, ub), + )) })?; let m = IntVarInner::constant(m); @@ -532,7 +564,7 @@ impl } } -impl TryFrom> +impl TryFrom> for IntVarInner { type Error = SynthesisError; @@ -575,22 +607,81 @@ impl EqGadget for IntVarInner { } Ok(()) } + + fn enforce_not_equal(&self, other: &Self) -> Result<(), SynthesisError> { + if self.limbs.len() != other.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.bounds.len() != other.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + for i in 0..self.limbs.len() { + if self.bounds[i] != other.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + self.limbs[i].enforce_not_equal(&other.limbs[i])?; + } + Ok(()) + } + + fn conditional_enforce_equal( + &self, + other: &Self, + should_enforce: &Boolean, + ) -> Result<(), SynthesisError> { + if should_enforce.is_constant() { + if should_enforce.value()? { + return self.enforce_equal(other); + } else { + return self.enforce_not_equal(other); + } + } + self.is_eq(other)? + .conditional_enforce_equal(&Boolean::TRUE, should_enforce) + } } impl FromBitsGadget for IntVarInner { - fn from_bits_le(bits: &[Boolean]) -> Result { + fn from_bits_le(bits: &[Boolean], bound: Bound) -> Result { Ok(Self::new( bits.chunks(F::BITS_PER_LIMB) .map(Boolean::le_bits_to_fp) .collect::>()?, - bits.chunks(F::BITS_PER_LIMB) - .map(|i| Bound(BigInt::zero(), (BigInt::one() << i.len()) - BigInt::one())) - .collect(), + compute_bounds(&bound.0, &bound.1, F::BITS_PER_LIMB), )) } } -impl ToBitsGadget for IntVarInner { +impl CondSelectGadget for IntVarInner { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.limbs.len() != false_value.limbs.len() { + return Err(SynthesisError::Unsatisfiable); + } + if true_value.bounds.len() != false_value.bounds.len() { + return Err(SynthesisError::Unsatisfiable); + } + let mut limbs = vec![]; + let mut bounds = vec![]; + for i in 0..true_value.limbs.len() { + if true_value.bounds[i] != false_value.bounds[i] { + return Err(SynthesisError::Unsatisfiable); + } + limbs.push(cond.select(&true_value.limbs[i], &false_value.limbs[i])?); + bounds.push(true_value.bounds[i].clone()); + } + Ok(Self { + _cfg: PhantomData, + limbs, + bounds, + }) + } +} + +impl ToBitsGadget for IntVarInner { fn to_bits_le(&self) -> Result>, SynthesisError> { for bound in &self.bounds { assert!(bound.0 >= BigInt::zero()); @@ -605,13 +696,13 @@ impl ToBitsGadget for IntVarInner { } } -impl AbsorbableGadget> for IntVarInner { +impl AbsorbableGadget for IntVarInner { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; self.to_bits_le()? .chunks(bits_per_limb) - .try_for_each(|i| Ok(dest.push(Boolean::le_bits_to_fp(i)?))) + .try_for_each(|i| Boolean::le_bits_to_fp(i).map(|v| dest.push(v))) } } @@ -694,6 +785,34 @@ impl MatrixGadget> } } +pub fn compute_bounds(lb: &BigInt, ub: &BigInt, bits_per_limb: usize) -> Vec { + let len = max(lb.bits(), ub.bits()) as usize; + let (n_full_limbs, n_remaining_bits) = len.div_rem(&bits_per_limb); + + let mut bounds = vec![ + Bound( + if lb.is_negative() { + BigInt::one() - (BigInt::one() << bits_per_limb) + } else { + BigInt::zero() + }, + if ub.is_positive() { + (BigInt::one() << bits_per_limb) - BigInt::one() + } else { + BigInt::zero() + }, + ); + n_full_limbs + ]; + + if !n_remaining_bits.is_zero() { + let d = BigInt::one() << (len - n_remaining_bits); + bounds.push(Bound(lb.div_floor(&d), ub.div_ceil(&d))); + } + + bounds +} + impl AllocVar<(BigInt, Bound), F> for IntVarInner { fn new_variable>( cs: impl Into>, @@ -728,8 +847,6 @@ impl AllocVar<(BigInt, Bound), F> for IntVarInner AllocVar<(BigInt, Bound), F> for IntVarInner>()?; - let mut bounds = vec![ - Bound( - if lb.is_negative() { - BigInt::one() - (BigInt::one() << F::BITS_PER_LIMB) - } else { - BigInt::zero() - }, - if ub.is_positive() { - (BigInt::one() << F::BITS_PER_LIMB) - BigInt::one() - } else { - BigInt::zero() - }, - ); - n_full_limbs - ]; - - if !n_remaining_bits.is_zero() { - let d = BigInt::one() << (len - n_remaining_bits); - bounds.push(Bound(lb.div_floor(&d), ub.div_ceil(&d))); - } + let bounds = compute_bounds(&lb, &ub, F::BITS_PER_LIMB); let var = Self::new(limbs, bounds); @@ -847,7 +945,7 @@ impl AllocVar for IntVarInner IntVarInner { - fn constant(x: BigInt) -> Self { + pub fn constant(x: BigInt) -> Self { Self::new_constant(ConstraintSystemRef::None, (x.clone(), Bound(x.clone(), x))).unwrap() } } @@ -1125,11 +1223,11 @@ mod tests { let aab = a * ab; let abb = ab * b; - let a_var = NonNativeFieldVar::::new_witness(cs.clone(), || Ok(a))?; - let b_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(b))?; - let ab_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(ab))?; - let aab_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(aab))?; - let abb_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(abb))?; + let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; + let b_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(b))?; + let ab_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(ab))?; + let aab_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(aab))?; + let abb_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(abb))?; a_var.mul_unaligned(&b_var)?.enforce_congruent(&ab_var)?; a_var.mul_unaligned(&ab_var)?.enforce_congruent(&aab_var)?; @@ -1147,17 +1245,14 @@ mod tests { let a = Fq::rand(rng); - let a_var = NonNativeFieldVar::::new_witness(cs.clone(), || Ok(a))?; + let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; let mut r_var = a_var.clone(); for _ in 0..16 { r_var = r_var.mul_unaligned(&r_var)?.modulo()?; } r_var = r_var.mul_unaligned(&a_var)?.modulo()?; - assert_eq!( - BigInt::from_biguint(Sign::Plus, a.pow([65537u64]).into()), - r_var.value()? - ); + assert_eq!(a.pow([65537u64]), r_var.value()?); assert!(cs.is_satisfied()?); Ok(()) } @@ -1173,12 +1268,12 @@ mod tests { let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); - let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; - let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; - let c_var = NonNativeFieldVar::new_witness(cs.clone(), || Ok(c))?; + let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; + let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; + let c_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(c))?; - let mut r_var: NonNativeFieldVar = - NonNativeFieldVar::constant(BigUint::zero().into()).into(); + let mut r_var: EmulatedFieldVar = + EmulatedFieldVar::constant(BigUint::zero().into()).into(); for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { r_var = r_var.add_unaligned(&a.mul_unaligned(&b)?)?; } diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs index ce2d734f4..4cd35cd6e 100644 --- a/crates/primitives/src/algebra/field/mod.rs +++ b/crates/primitives/src/algebra/field/mod.rs @@ -4,21 +4,19 @@ use ark_relations::gr1cs::SynthesisError; use ark_std::{any::TypeId, mem::transmute_copy}; use crate::{ - traits::{Inputize, InputizeNonNative}, + algebra::{field::emulated::EmulatedFieldVar, Val}, + traits::{Inputize, InputizeEmulated}, transcripts::{Absorbable, AbsorbableGadget}, }; -// pub mod nonnative; -pub mod nonnative2; +pub mod emulated; /// `Field` trait is a wrapper around `PrimeField` that also includes the /// necessary bounds for the field to be used conveniently in folding schemes. pub trait SonobeField: - PrimeField + Absorbable + Inputize + PrimeField + Absorbable + Inputize + Val> { const BITS_PER_LIMB: usize; - /// The in-circuit variable type for this field. - type Var: FieldVar; } impl, const N: usize> SonobeField for Fp { @@ -41,11 +39,17 @@ impl, const N: usize> SonobeField for Fp { // TODO: either make it a global const, or compute an optimal value // based on the modulus size. const BITS_PER_LIMB: usize = 55; // TODO: make this configurable +} + +impl, const N: usize> Val for Fp { + type ConstraintField = Self; type Var = FpVar; + + type EmulatedVar = EmulatedFieldVar; } -impl, const N: usize> Absorbable for Fp { - fn absorb_into(&self, dest: &mut Vec) { +impl, const N: usize> Absorbable for Fp { + fn absorb_into(&self, dest: &mut Vec) { if TypeId::of::() == TypeId::of::() { // Safe because `F` and `Self` have the same type // TODO (@winderica): specialization when??? @@ -67,7 +71,7 @@ impl, const N: usize> Absorbable for Fp { } } -impl AbsorbableGadget> for FpVar { +impl AbsorbableGadget for FpVar { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { dest.push(self.clone()); Ok(()) @@ -82,10 +86,10 @@ impl, const N: usize> Inputize for Fp { } } -impl InputizeNonNative for P { +impl InputizeEmulated for P { /// Returns the internal representation in the same order as how the value /// is allocated in `NonNativeUintVar::new_input`. - fn inputize_nonnative(&self) -> Vec { + fn inputize_emulated(&self) -> Vec { self.into_bigint() .to_bits_le() .chunks(F::BITS_PER_LIMB) diff --git a/crates/primitives/src/algebra/field/nonnative.rs b/crates/primitives/src/algebra/field/nonnative.rs deleted file mode 100644 index 006d8fd0f..000000000 --- a/crates/primitives/src/algebra/field/nonnative.rs +++ /dev/null @@ -1,1232 +0,0 @@ -use ark_ff::{BigInteger, One, PrimeField, Zero}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - boolean::Boolean, - convert::ToBitsGadget, - fields::{fp::FpVar, FieldVar}, - prelude::EqGadget, - select::CondSelectGadget, - GR1CSVar, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{ - borrow::Borrow, - cmp::{max, min}, - ops::Index, -}; -use num_bigint::{BigInt, BigUint, Sign}; -use num_integer::Integer; -use num_traits::Signed; - -use crate::algebra::ops::bits::{FromBitsGadget, ToBitsGadgetExt}; -use crate::{ - algebra::{ - field::SonobeField, - ops::{ - eq::EquivalenceGadget, - matrix::{MatrixGadget, SparseMatrixVar}, - vector::VectorGadget, - }, - }, - transcripts::AbsorbableGadget, -}; - -#[derive(Debug, Default, Clone, PartialEq)] -pub struct Bound { - pub lb: BigInt, - pub ub: BigInt, -} - -impl Bound { - pub fn zero() -> Self { - Self::default() - } - - pub fn new_ub(ub: BigInt) -> Self { - Self { - lb: BigInt::zero(), - ub, - } - } - - pub fn new_n_bits(n: usize) -> Self { - Self { - lb: BigInt::zero(), - ub: (BigInt::one() << n) - BigInt::one(), - } - } - - pub fn new(lb: BigInt, ub: BigInt) -> Self { - Self { lb, ub } - } -} - -impl Bound { - pub fn add(&self, other: &Self) -> Self { - Self { - lb: &self.lb + &other.lb, - ub: &self.ub + &other.ub, - } - } - - pub fn sub(&self, other: &Self) -> Self { - Self { - lb: &self.lb - &other.ub, - ub: &self.ub - &other.lb, - } - } - - pub fn add_many(limbs: &[Self]) -> Self { - Self { - lb: limbs.iter().map(|l| &l.lb).sum(), - ub: limbs.iter().map(|l| &l.ub).sum(), - } - } - - pub fn mul(&self, other: &Self) -> Self { - let ll = &self.lb * &other.lb; - let lu = &self.lb * &other.ub; - let ul = &self.ub * &other.lb; - let uu = &self.ub * &other.ub; - - Self { - lb: min(min(&ll, &lu), min(&ul, &uu)).clone(), - ub: max(max(&ll, &lu), max(&ul, &uu)).clone(), - } - } - - pub fn shl(&self, shift: usize) -> Self { - Self { - lb: &self.lb << shift, - ub: &self.ub << shift, - } - } - - pub fn filter_safe(self) -> Option { - let limit = BigInt::from_biguint(Sign::Plus, F::MODULUS_MINUS_ONE_DIV_TWO.into()); - (self.ub <= limit && self.lb >= -limit).then_some(self) - } -} - -// /// `LimbVar` represents a single limb of a non-native unsigned integer in the -// /// circuit. -// /// The limb value `v` should be small enough to fit into `FpVar`, and we also -// /// store an upper bound `ub` for the limb value, which is treated as a constant -// /// in the circuit and is used for efficient equality checks and some arithmetic -// /// operations. -// #[derive(Debug, Clone)] -// pub struct LimbVar { -// pub v: FpVar, -// pub lb: BigInt, -// pub ub: BigInt, -// } - -// impl]>> From for LimbVar { -// fn from(bits: B) -> Self { -// Self { -// // `Boolean::le_bits_to_fp` will return an error if the internal -// // invocation of `Boolean::enforce_in_field_le` fails. -// // However, this method is only called when the length of `bits` is -// // greater than `F::MODULUS_BIT_SIZE`, which should not happen in -// // our case where `bits` is guaranteed to be short. -// v: Boolean::le_bits_to_fp(bits.as_ref()).unwrap(), -// lb: BigInt::zero(), -// ub: (BigInt::one() << bits.as_ref().len()) - BigInt::one(), -// } -// } -// } - -// impl Default for LimbVar { -// fn default() -> Self { -// Self { -// v: FpVar::zero(), -// lb: BigInt::zero(), -// ub: BigInt::zero(), -// } -// } -// } - -// impl GR1CSVar for LimbVar { -// type Value = F; - -// fn cs(&self) -> ConstraintSystemRef { -// self.v.cs() -// } - -// fn value(&self) -> Result { -// self.v.value() -// } -// } - -// impl CondSelectGadget for LimbVar { -// fn conditionally_select( -// cond: &Boolean, -// true_value: &Self, -// false_value: &Self, -// ) -> Result { -// // We only allow selecting between two values with the same upper bound -// assert_eq!(true_value.lb, false_value.lb); -// assert_eq!(true_value.ub, false_value.ub); -// Ok(Self { -// v: cond.select(&true_value.v, &false_value.v)?, -// lb: true_value.lb.clone(), -// ub: true_value.ub.clone(), -// }) -// } -// } - -// impl LimbVar { -// /// Add two `LimbVar`s. -// /// Returns `None` if the upper bound of the sum is too large, i.e., -// /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. -// /// Otherwise, returns the sum as a `LimbVar`. -// pub fn add(&self, other: &Self) -> Option { -// let lbound = &self.lb + &other.lb; -// let ubound = &self.ub + &other.ub; -// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); -// if ubound > limit || lbound < -limit { -// None -// } else { -// Some(Self { -// v: &self.v + &other.v, -// lb: lbound, -// ub: ubound, -// }) -// } -// } - -// /// Add multiple `LimbVar`s. -// /// Returns `None` if the upper bound of the sum is too large, i.e., -// /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. -// /// Otherwise, returns the sum as a `LimbVar`. -// pub fn add_many(limbs: &[Self]) -> Option { -// let lbound = limbs.iter().map(|l| &l.lb).sum::(); -// let ubound = limbs.iter().map(|l| &l.ub).sum::(); -// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); -// if ubound > limit || lbound < -limit { -// None -// } else { -// Some(Self { -// v: if limbs.is_constant() { -// FpVar::constant(limbs.value().unwrap_or_default().into_iter().sum()) -// } else { -// limbs.iter().map(|l| &l.v).sum() -// }, -// lb: lbound, -// ub: ubound, -// }) -// } -// } - -// /// Multiply two `LimbVar`s. -// /// Returns `None` if the upper bound of the product is too large, i.e., -// /// greater than `F::MODULUS_MINUS_ONE_DIV_TWO`. -// /// Otherwise, returns the product as a `LimbVar`. -// pub fn mul(&self, other: &Self) -> Option { -// let ll = &self.lb * &other.lb; -// let lu = &self.lb * &other.ub; -// let ul = &self.ub * &other.lb; -// let uu = &self.ub * &other.ub; - -// let lbound = min(min(&ll, &lu), min(&ul, &uu)).clone(); -// let ubound = max(max(&ll, &lu), max(&ul, &uu)).clone(); -// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); -// if ubound > limit || lbound < -limit { -// None -// } else { -// Some(Self { -// v: &self.v * &other.v, -// lb: lbound, -// ub: ubound, -// }) -// } -// } - -// pub fn zero() -> Self { -// Self::default() -// } - -// pub fn constant(v: BigInt) -> Self { -// let (v_sign, v_abs) = v.clone().into_parts(); -// let limit = Into::::into(F::MODULUS_MINUS_ONE_DIV_TWO).into(); -// assert!(v_abs <= limit); -// Self { -// v: if v_sign == Sign::Minus { -// FpVar::constant(-F::from(v_abs)) -// } else { -// FpVar::constant(F::from(v_abs)) -// }, -// lb: v.clone(), -// ub: v, -// } -// } -// } - -// impl ToBitsGadget for LimbVar { -// fn to_bits_le(&self) -> Result>, SynthesisError> { -// let cs = self.cs(); - -// assert_eq!(self.lb, BigInt::zero()); - -// let bits = &self -// .v -// .value() -// .unwrap_or_default() -// .into_bigint() -// .to_bits_le()[..self.ub.bits() as usize]; -// let bits = if cs.is_none() { -// Vec::new_constant(cs, bits)? -// } else { -// Vec::new_witness(cs, || Ok(bits))? -// }; - -// Boolean::le_bits_to_fp(&bits)?.enforce_equal(&self.v)?; - -// Ok(bits) -// } -// } - -/// `NonNativeUintVar` represents a non-native unsigned integer (BigUint) in the -/// circuit. -/// We apply [xJsnark](https://akosba.github.io/papers/xjsnark.pdf)'s techniques -/// for efficient operations on `NonNativeUintVar`. -/// Note that `NonNativeUintVar` is different from arkworks' `NonNativeFieldVar` -/// in that the latter runs the expensive `reduce` (`align` + `modulo` in our -/// terminology) after each arithmetic operation, while the former only reduces -/// the integer when explicitly called. -#[derive(Debug, Clone)] -pub struct NonNativeUintVar { - pub(crate) limbs: Vec>, - bounds: Vec, -} - -impl AllocVar<(BigInt, Bound), F> for NonNativeUintVar { - fn new_variable>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - todo!(); - let cs = cs.into().cs(); - let v = f()?; - let (x, b) = v.borrow(); - - if x > &b.ub || x < &b.lb { - return Err(SynthesisError::DivisionByZero); - } - - let (x_sign, mut x_abs) = x.clone().into_parts(); - - let mut limbs = vec![]; - let mut bounds = vec![]; - - let l = max(b.lb.bits(), b.ub.bits()); - - if l == 0 { - return Ok(Self { limbs, bounds }); - } - - let (num_full_chunks, final_chunk_size) = (l as usize).div_rem(&F::BITS_PER_LIMB); - - let mask = (BigUint::one() << F::BITS_PER_LIMB) - BigUint::one(); - - loop { - let limb = FpVar::new_variable(cs.clone(), || Ok(F::from(&x_abs & &mask)), mode)?; - Self::enforce_bit_length(&limb, F::BITS_PER_LIMB)?; - limbs.push(limb); - bounds.push(Bound::new_n_bits(F::BITS_PER_LIMB)); - x_abs >>= F::BITS_PER_LIMB; - if x_abs.is_zero() { - let is_neg = Boolean::new_variable(cs.clone(), || Ok(x_sign == Sign::Minus), { - if b.lb >= BigInt::zero() || b.ub <= BigInt::zero() { - AllocationMode::Constant - } else { - mode - } - })?; - *limbs.last_mut().unwrap() *= - is_neg.select(&FpVar::one().negate()?, &FpVar::one())?; - break; - } - } - - if final_chunk_size > 0 { - let limb = FpVar::new_variable(cs.clone(), || Ok(F::from(x_abs)), mode)?; - Self::enforce_bit_length(&limb, F::BITS_PER_LIMB)?; - } - - for chunk in (0..l) - .map(|i| x.bit(i)) - .collect::>() - .chunks(F::BITS_PER_LIMB) - { - let limb = F::from(F::BigInt::from_bits_le(chunk)); - let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; - Self::enforce_bit_length(&limb, chunk.len())?; - limbs.push(limb); - bounds.push(Bound::new_n_bits(chunk.len())); - } - let s = Boolean::new_variable(cs.clone(), || Ok(x.sign() == Sign::Minus), { - if b.lb >= BigInt::zero() || b.ub <= BigInt::zero() { - AllocationMode::Constant - } else { - mode - } - })?; - limbs[num_full_chunks] = - s.select(&limbs[num_full_chunks].negate()?, &limbs[num_full_chunks])?; - - let t = BigInt::one() << ((bounds.len() - 1) * F::BITS_PER_LIMB); - bounds[num_full_chunks] = Bound::new(b.lb.div_floor(&t), b.ub.div_ceil(&t)); - - Ok(Self { limbs, bounds }) - } -} - -impl AllocVar for NonNativeUintVar { - fn new_variable>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let cs = cs.into().cs(); - let v = f()?; - - let v = BigInt::from_biguint(Sign::Plus, v.borrow().clone().into()); - let m = BigInt::from_biguint(Sign::Plus, G::MODULUS.into()); - - match mode { - AllocationMode::Constant => Self::constant(v), - _ => Self::new_variable(cs, || Ok((v, Bound::new(BigInt::zero(), m))), mode), - } - } -} - -impl EqGadget for NonNativeUintVar { - fn is_eq(&self, other: &Self) -> Result, SynthesisError> { - let mut result = Boolean::TRUE; - if self.limbs.len() != other.limbs.len() { - return Err(SynthesisError::Unsatisfiable); - } - if self.bounds.len() != other.bounds.len() { - return Err(SynthesisError::Unsatisfiable); - } - for i in 0..self.limbs.len() { - if self.bounds[i] != other.bounds[i] { - return Err(SynthesisError::Unsatisfiable); - } - result &= self.limbs[i].is_eq(&other.limbs[i])?; - } - Ok(result) - } - - fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { - if self.limbs.len() != other.limbs.len() { - return Err(SynthesisError::Unsatisfiable); - } - if self.bounds.len() != other.bounds.len() { - return Err(SynthesisError::Unsatisfiable); - } - for i in 0..self.limbs.len() { - if self.bounds[i] != other.bounds[i] { - return Err(SynthesisError::Unsatisfiable); - } - self.limbs[i].enforce_equal(&other.limbs[i])?; - } - Ok(()) - } -} - -impl GR1CSVar for NonNativeUintVar { - type Value = BigInt; - - fn cs(&self) -> ConstraintSystemRef { - self.limbs.cs() - } - - fn value(&self) -> Result { - let mut r = BigInt::zero(); - - for limb in self.limbs.value()?.into_iter().rev() { - r <<= F::BITS_PER_LIMB; - r += if limb.into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO { - BigInt::from_biguint(Sign::Minus, (-limb).into()) - } else { - BigInt::from_biguint(Sign::Plus, limb.into()) - }; - } - - Ok(r) - } -} - -impl CondSelectGadget for NonNativeUintVar { - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - if true_value.limbs.len() != false_value.limbs.len() { - return Err(SynthesisError::Unsatisfiable); - } - if true_value.bounds.len() != false_value.bounds.len() { - return Err(SynthesisError::Unsatisfiable); - } - let mut v = vec![]; - let mut bounds = vec![]; - for i in 0..true_value.limbs.len() { - if true_value.bounds[i] != false_value.bounds[i] { - return Err(SynthesisError::Unsatisfiable); - } - v.push(cond.select(&true_value.limbs[i], &false_value.limbs[i])?); - bounds.push(true_value.bounds[i].clone()); - } - Ok(Self { - limbs: v, - bounds: bounds, - }) - } -} - -impl NonNativeUintVar { - fn constant(v: BigInt) -> Result { - Self::new_constant( - ConstraintSystemRef::None, - (v.clone(), Bound::new(v.clone(), v)), - ) - } - - fn ubound(&self) -> BigInt { - let mut r = BigInt::zero(); - - for i in self.bounds.iter().rev() { - r <<= F::BITS_PER_LIMB; - r += &i.ub; - } - - r - } - - fn lbound(&self) -> BigInt { - let mut r = BigInt::zero(); - - for i in self.bounds.iter().rev() { - r <<= F::BITS_PER_LIMB; - r += &i.lb; - } - - r - } -} - -impl NonNativeUintVar { - /// Enforce `self` to be less than `other`, where `self` and `other` should - /// be aligned. - /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 - pub fn enforce_lt(&self, other: &Self) -> Result<(), SynthesisError> { - let len = max(self.limbs.len(), other.limbs.len()); - let zero = FpVar::zero(); - - // Compute the difference between limbs of `other` and `self`. - // Denote a positive limb by `+`, a negative limb by `-`, a zero limb by - // `0`, and an unknown limb by `?`. - // Then, for `self < other`, `delta` should look like: - // ? ? ... ? ? + 0 0 ... 0 0 - let delta = (0..len) - .map(|i| { - let x = self.limbs.get(i).unwrap_or(&zero); - let y = other.limbs.get(i).unwrap_or(&zero); - y - x - }) - .collect::>(); - - // `helper` is a vector of booleans that indicates if the corresponding - // limb of `delta` is the first (searching from MSB) positive limb. - // For example, if `delta` is: - // - + ... + - + 0 0 ... 0 0 - // <---- search in this direction -------- - // Then `helper` should be: - // F F ... F F T F F ... F F - let helper = { - let cs = self.cs().or(other.cs()); - let mut helper = vec![false; len]; - for i in (0..len).rev() { - let delta = delta[i].value().unwrap_or_default().into_bigint(); - if !delta.is_zero() && delta < F::MODULUS_MINUS_ONE_DIV_TWO { - helper[i] = true; - break; - } - } - Vec::>::new_variable_with_inferred_mode(cs, || Ok(helper))? - }; - - // `p` is the first positive limb in `delta`. - let mut p = FpVar::::zero(); - // `r` is the sum of all bits in `helper`, which should be 1 when `self` - // is less than `other`, as there should be more than one positive limb - // in `delta`, and thus exactly one true bit in `helper`. - let mut r = FpVar::zero(); - for (b, d) in helper.into_iter().zip(delta) { - // Choose the limb `d` only if `b` is true. - p += b.select(&d, &FpVar::zero())?; - // Either `r` or `d` should be zero. - // Consider the same example as above: - // - + ... + - + 0 0 ... 0 0 - // F F ... F F T F F ... F F - // |-----------| - // `r = 0` in this range (before/when we meet the first positive limb) - // |---------| - // `d = 0` in this range (after we meet the first positive limb) - // This guarantees that for every bit after the true bit in `helper`, - // the corresponding limb in `delta` is zero. - (&r * &d).enforce_equal(&FpVar::zero())?; - // Add the current bit to `r`. - r += FpVar::from(b); - } - - // Ensure that `r` is exactly 1. This guarantees that there is exactly - // one true value in `helper`. - r.enforce_equal(&FpVar::one())?; - // Ensure that `p` is positive, i.e., - // `0 <= p - 1 < 2^bits_per_limb < F::MODULUS_MINUS_ONE_DIV_TWO`. - // This guarantees that the true value in `helper` corresponds to a - // positive limb in `delta`. - Self::enforce_bit_length(&(p - FpVar::one()), F::BITS_PER_LIMB)?; - - Ok(()) - } - - /// Enforce `self` to be equal to `other`, where `self` and `other` are not - /// necessarily aligned. - /// - /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L562-L798 - /// Similar implementations can also be found in https://github.com/alex-ozdemir/bellman-bignat/blob/0585b9d90154603a244cba0ac80b9aafe1d57470/src/mp/bignat.rs#L566-L661 - /// and https://github.com/arkworks-rs/r1cs-std/blob/4020fbc22625621baa8125ede87abaeac3c1ca26/src/fields/emulated_fp/reduce.rs#L201-L323 - pub fn enforce_equal_unaligned(&self, other: &Self) -> Result<(), SynthesisError> { - let len = min(self.limbs.len(), other.limbs.len()); - - // Group the limbs of `self` and `other` so that each group nearly - // reaches the capacity `F::MODULUS_MINUS_ONE_DIV_TWO`. - // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` - // is the initial number of bits in a limb, just as what we do in grade - // school arithmetic, e.g., - // 5 9 - // x 7 3 - // ------------- - // 15 27 - // 35 63 - // ------------- <- When grouping 35, 15 + 63, and 27, we are computing - // 4 3 0 7 35 * 100 + (15 + 63) * 10 + 27 = 4307 - // Note that this is different from the concatenation `x_0 || x_1 ...`, - // since the bit-length of each limb is not necessarily the initial size - // `W`. - - let mut i = 0; - // `c` stores the current carry of `x_i - y_i` - let mut c = FpVar::::zero(); - while i < len { - let mut j = i; - // The current grouped limbs of `self` and `other`. - let mut p_limb = FpVar::zero(); - let mut q_limb = FpVar::zero(); - let mut p_bound = Bound::zero(); - let mut q_bound = Bound::zero(); - let mut step = 0; - let mut weight = F::one(); - while j < len { - match ( - self.bounds[j].shl(step).add(&p_bound).filter_safe::(), - other.bounds[j].shl(step).add(&q_bound).filter_safe::(), - ) { - (Some(new_p_bound), Some(new_q_bound)) => { - p_limb += &self.limbs[j] * weight; - q_limb += &other.limbs[j] * weight; - p_bound = new_p_bound; - q_bound = new_q_bound; - } - _ => break, - } - - j += 1; - step += F::BITS_PER_LIMB; - weight *= F::from(BigUint::one() << F::BITS_PER_LIMB); - } - // For each group, check the last `step_i` bits of `x_i` and `y_i` are - // equal. - // The intuition is to check `diff = x_i - y_i = 0 (mod 2^step_i)`. - // However, this is only true for `i = 0`, and we need to consider carry - // values `diff >> step_i` for `i > 0`. - // Therefore, we actually check `diff = x_i - y_i + c = 0 (mod 2^step_i)` - // and derive the next `c` by computing `diff >> step_i`. - // To enforce `diff = 0 (mod 2^step_i)`, we compute `diff / 2^step_i` - // and enforce it to be small (soundness holds because for `a` that does - // not divide `b`, `b / a` in the field will be very large). - c = (&p_limb - &q_limb + &c) * weight.inverse().unwrap(); - if j < len { - // Unlike the code mentioned above which add some offset to the - // diff `x_i - y_i + c` to make it always positive, we directly - // check if the absolute value of the diff is small. - Self::enforce_abs_bit_length( - &c, - (max( - min(&p_bound.lb, &q_bound.lb).bits(), - max(&p_bound.ub, &q_bound.ub).bits(), - ) as usize) - .checked_sub(step) - .unwrap_or_default(), - )?; - } else { - let remaining_limbs = &(if j < self.limbs.len() { self } else { other }).limbs[j..]; - let remaining_bounds = - &(if j < self.bounds.len() { self } else { other }).bounds[j..]; - if remaining_limbs.is_empty() { - c.enforce_equal(&FpVar::zero())?; - } else { - // If there is any remaining limb, the first one should be the - // final carry (which will be checked later), and the following - // ones should be zero. - - // Enforce the remaining limbs to be zero. - // Instead of doing that one by one, we check if their sum is - // zero using a single constraint. - // This is sound, as the upper bounds of the limbs and their sum - // are guaranteed to be less than `F::MODULUS_MINUS_ONE_DIV_TWO` - // (i.e., all of them are "non-negative"), implying that all - // limbs should be zero to make the sum zero. - remaining_limbs[1..] - .iter() - .sum::>() - .enforce_equal(&FpVar::zero())?; - Bound::add_many(remaining_bounds) - .filter_safe::() - .ok_or(SynthesisError::Unsatisfiable)?; - // For the final carry, we need to ensure that it equals the - // remaining limb `rest`. - c.enforce_equal(&remaining_limbs[0])?; - }; - } - // Start the next group - i = j; - } - - Ok(()) - } -} - -impl NonNativeUintVar { - fn enforce_bit_length(x: &FpVar, length: usize) -> Result>, SynthesisError> { - let cs = x.cs(); - - let bits = &x.value().unwrap_or_default().into_bigint().to_bits_le()[..length]; - let bits = if cs.is_none() { - Vec::new_constant(cs, bits)? - } else { - Vec::new_witness(cs, || Ok(bits))? - }; - - Boolean::le_bits_to_fp(&bits)?.enforce_equal(x)?; - - Ok(bits) - } - - fn enforce_abs_bit_length( - x: &FpVar, - length: usize, - ) -> Result>, SynthesisError> { - let cs = x.cs(); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - - let is_neg = Boolean::new_variable( - cs.clone(), - || Ok(x.value().unwrap_or_default().into_bigint() > F::MODULUS_MINUS_ONE_DIV_TWO), - mode, - )?; - let bits = Vec::new_variable( - cs.clone(), - || { - Ok({ - let x = x.value().unwrap_or_default(); - let mut bits = if is_neg.value().unwrap_or_default() { - -x - } else { - x - } - .into_bigint() - .to_bits_le(); - bits.resize(length, false); - bits - }) - }, - mode, - )?; - - // Below is equivalent to but more efficient than - // `Boolean::le_bits_to_fp(&bits)?.enforce_equal(&is_neg.select(&x.negate()?, &x)?)?` - // Note that this enforces: - // 1. The claimed absolute value `is_neg.select(&x.negate()?, &x)?` has - // exactly `length` bits. - // 2. `is_neg` is indeed the sign of `x`, i.e., `is_neg = false` when - // `0 <= x < (|F| - 1) / 2`, and `is_neg = true` when - // `(|F| - 1) / 2 <= x < F`, thus the claimed absolute value is - // correct. - // If `is_neg` is incorrect, then: - // a. `0 <= x < (|F| - 1) / 2`, but `is_neg = true`, then - // `is_neg.select(&x.negate()?, &x)?` returns `|F| - x`, - // which is greater than `(|F| - 1) / 2` and cannot fit in - // `length` bits (given that `length` is small). - // b. `(|F| - 1) / 2 <= x < F`, but `is_neg = false`, then - // `is_neg.select(&x.negate()?, &x)?` returns `x`, which is - // greater than `(|F| - 1) / 2` and cannot fit in `length` - // bits. - FpVar::from(is_neg).mul_equals(&x.double()?, &(x - Boolean::le_bits_to_fp(&bits)?))?; - - Ok(bits) - } - - /// Compute `self + other`, without aligning the limbs. - pub fn add_no_align(&self, other: &Self) -> Result { - let mut z = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; - let mut bounds = vec![Bound::zero(); z.len()]; - for (i, v) in self.limbs.iter().enumerate() { - bounds[i] = bounds[i] - .add(&self.bounds[i]) - .filter_safe::() - .ok_or(SynthesisError::Unsatisfiable)?; - z[i] += v; - } - for (i, v) in other.limbs.iter().enumerate() { - bounds[i] = bounds[i] - .add(&other.bounds[i]) - .filter_safe::() - .ok_or(SynthesisError::Unsatisfiable)?; - z[i] += v; - } - Ok(Self { - limbs: z, - bounds: bounds, - }) - } - - pub fn sub_no_align(&self, other: &Self) -> Result { - let mut z = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; - let mut bounds = vec![Bound::zero(); z.len()]; - for (i, v) in self.limbs.iter().enumerate() { - bounds[i] = bounds[i] - .add(&self.bounds[i]) - .filter_safe::() - .ok_or(SynthesisError::Unsatisfiable)?; - z[i] += v; - } - for (i, v) in other.limbs.iter().enumerate() { - bounds[i] = bounds[i] - .sub(&other.bounds[i]) - .filter_safe::() - .ok_or(SynthesisError::Unsatisfiable)?; - z[i] -= v; - } - Ok(Self { - limbs: z, - bounds: bounds, - }) - } - - /// Compute `self * other`, without aligning the limbs. - /// Implements the O(n) approach described in xJsnark, Section IV.B.1) - pub fn mul_no_align(&self, other: &Self) -> Result { - let len = self.limbs.len() + other.limbs.len() - 1; - if self.is_constant() || other.is_constant() { - // Use the naive approach for constant operands, which costs no - // constraints. - let bounds = (0..len) - .map(|i| { - let start = max(i + 1, other.bounds.len()) - other.bounds.len(); - let end = min(i + 1, self.bounds.len()); - Bound::add_many( - &(start..end) - .map(|j| self.bounds[j].mul(&other.bounds[i - j])) - .collect::>(), - ) - .filter_safe::() - }) - .collect::>>() - .ok_or(SynthesisError::Unsatisfiable)?; - - let z = (0..len) - .map(|i| { - let start = max(i + 1, other.limbs.len()) - other.limbs.len(); - let end = min(i + 1, self.limbs.len()); - (start..end) - .map(|j| &self.limbs[j] * &other.limbs[i - j]) - .sum() - }) - .collect(); - return Ok(Self { - limbs: z, - bounds: bounds, - }); - } - let cs = self.cs().or(other.cs()); - let mode = if cs.is_none() { - AllocationMode::Constant - } else { - AllocationMode::Witness - }; - - // Compute the result `z` outside the circuit and provide it as hints. - let (z, bounds) = { - let mut z = vec![F::zero(); len]; - let mut bounds = vec![Bound::zero(); len]; - for i in 0..self.limbs.len() { - for j in 0..other.limbs.len() { - z[i + j] += self.limbs[i].value().unwrap_or_default() - * other.limbs[j].value().unwrap_or_default(); - bounds[i + j] = bounds[i + j].add(&self.bounds[i].mul(&other.bounds[j])) - } - } - ( - Vec::new_variable(cs.clone(), || Ok(z), mode)?, - bounds - .into_iter() - .map(|b| b.filter_safe::()) - .collect::>() - .ok_or(SynthesisError::Unsatisfiable)?, - ) - }; - for c in 1..=len { - let c = F::from(c as u64); - let mut t = F::one(); - let mut c_powers = vec![]; - for _ in 0..len { - c_powers.push(t); - t *= c; - } - // `l = Σ self[i] c^i` - let l = self - .limbs - .iter() - .zip(&c_powers) - .map(|(v, t)| v * *t) - .sum::>(); - // `r = Σ other[i] c^i` - let r = other - .limbs - .iter() - .zip(&c_powers) - .map(|(v, t)| v * *t) - .sum::>(); - // `o = Σ z[i] c^i` - let o = z - .iter() - .zip(&c_powers) - .map(|(v, t)| v * *t) - .sum::>(); - // Enforce `o = l * r` - l.mul_equals(&r, &o)?; - } - - Ok(Self { - limbs: z, - bounds: bounds, - }) - } - - /// Convert `Self` to an element in `M`, i.e., compute `Self % M::MODULUS`. - pub fn modulo(&self) -> Result { - let cs = self.cs(); - let m = BigInt::from_biguint(Sign::Plus, M::MODULUS.into()); - // Provide the quotient and remainder as hints - let q = Self::new_variable_with_inferred_mode(cs.clone(), || { - let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); - Ok((self.value()?.div_floor(&m), Bound::new(lb, ub))) - })?; - let r = Self::new_variable_with_inferred_mode(cs.clone(), || { - Ok((self.value()?.abs() % &m, Bound::new_ub(m.clone()))) - })?; - - let m = Self::constant(m)?; - - // Enforce `self = q * m + r` - q.mul_no_align(&m)? - .add_no_align(&r)? - .enforce_equal_unaligned(self)?; - // Enforce `r < m` (and `r >= 0` already holds) - r.enforce_lt(&m)?; - - Ok(r) - } - - /// Enforce that `self` is congruent to `other` modulo `M::MODULUS`. - pub fn enforce_congruent(&self, other: &Self) -> Result<(), SynthesisError> { - let cs = self.cs(); - let m = BigInt::from_biguint(Sign::Plus, M::MODULUS.into()); - // Provide the quotient as hint - let q = Self::new_variable_with_inferred_mode(cs.clone(), || { - let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); - Ok((self.value()?.div_floor(&m), Bound::new(lb, ub))) - })?; - - let m = Self::constant(m)?; - - // Enforce `self - other = q * m` - self.sub_no_align(other)? - .enforce_equal_unaligned(&q.mul_no_align(&m)?) - } -} - -impl EquivalenceGadget for NonNativeUintVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { - self.enforce_congruent::(other) - } -} - -impl FromBitsGadget for NonNativeUintVar { - fn from_bits_le(bits: &[Boolean]) -> Result { - Ok(Self { - limbs: bits - .as_ref() - .chunks(F::BITS_PER_LIMB) - .map(Boolean::le_bits_to_fp) - .collect::>()?, - bounds: bits - .as_ref() - .chunks(F::BITS_PER_LIMB) - .map(|i| Bound::new_n_bits(i.len())) - .collect(), - }) - } -} - -impl ToBitsGadget for NonNativeUintVar { - fn to_bits_le(&self) -> Result>, SynthesisError> { - Ok(self - .limbs - .iter() - .map(|limb| limb.to_n_bits_le(F::BITS_PER_LIMB)) - .collect::, _>>()? - .concat()) - } -} - -impl AbsorbableGadget> for NonNativeUintVar { - fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { - let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; - - self.to_bits_le()? - .chunks(bits_per_limb) - .try_for_each(|i| Ok(dest.push(Boolean::le_bits_to_fp(i)?))) - } -} - -impl VectorGadget> for [NonNativeUintVar] { - fn add(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.add_no_align(y)) - .collect() - } - - fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.mul_no_align(y)) - .collect() - } - - fn scale( - &self, - other: &NonNativeUintVar, - ) -> Result>, SynthesisError> { - self.iter().map(|x| x.mul_no_align(other)).collect() - } -} - -impl MatrixGadget> for SparseMatrixVar> { - fn mul_vector( - &self, - v: &impl Index>, - ) -> Result>, SynthesisError> { - self.0 - .iter() - .map(|row| { - let len = row - .iter() - .map(|(value, col_i)| value.limbs.len() + v[*col_i].limbs.len() - 1) - .max() - .unwrap_or(0); - // This is a combination of `mul_no_align` and `add_no_align` - // that results in more flattened `LinearCombination`s. - // Consequently, `ConstraintSystem::inline_all_lcs` costs less - // time, thus making trusted setup and proof generation faster. - let bounds = (0..len) - .map(|i| { - Bound::add_many( - &row.iter() - .flat_map(|(value, col_i)| { - let start = - max(i + 1, v[*col_i].bounds.len()) - v[*col_i].bounds.len(); - let end = min(i + 1, value.bounds.len()); - (start..end) - .map(|j| value.bounds[j].mul(&v[*col_i].bounds[i - j])) - }) - .collect::>(), - ) - .filter_safe::() - }) - .collect::>>() - .ok_or(SynthesisError::Unsatisfiable)?; - let v = (0..len) - .map(|i| { - row.iter() - .flat_map(|(value, col_i)| { - let start = - max(i + 1, v[*col_i].limbs.len()) - v[*col_i].limbs.len(); - let end = min(i + 1, value.limbs.len()); - (start..end).map(|j| &value.limbs[j] * &v[*col_i].limbs[i - j]) - }) - .sum() - }) - .collect(); - Ok(NonNativeUintVar { - limbs: v, - bounds: bounds, - }) - }) - .collect() - } -} - -#[cfg(test)] -mod tests { - use ark_ff::Field; - use ark_pallas::{Fq, Fr}; - use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{test_rng, UniformRand, error::Error}; - use num_bigint::RandBigInt; - - use super::*; - - #[test] - fn test_mul_biguint() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let size = 256; - - let rng = &mut test_rng(); - let a = rng.gen_biguint(size as u64); - let b = rng.gen_biguint(size as u64); - let ab = &a * &b; - let aab = &a * &ab; - let abb = &ab * &b; - - let a_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok((a.into(), Bound::new_n_bits(size))))?; - let b_var = - NonNativeUintVar::new_witness(cs.clone(), || Ok((b.into(), Bound::new_n_bits(size))))?; - let ab_var = NonNativeUintVar::new_witness(cs.clone(), || { - Ok((ab.into(), Bound::new_n_bits(size * 2))) - })?; - let aab_var = NonNativeUintVar::new_witness(cs.clone(), || { - Ok((aab.into(), Bound::new_n_bits(size * 3))) - })?; - let abb_var = NonNativeUintVar::new_witness(cs.clone(), || { - Ok((abb.into(), Bound::new_n_bits(size * 3))) - })?; - - a_var - .mul_no_align(&b_var)? - .enforce_equal_unaligned(&ab_var)?; - a_var - .mul_no_align(&ab_var)? - .enforce_equal_unaligned(&aab_var)?; - ab_var - .mul_no_align(&b_var)? - .enforce_equal_unaligned(&abb_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_mul_fq() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let rng = &mut test_rng(); - let a = Fq::rand(rng); - let b = Fq::rand(rng); - let ab = a * b; - let aab = a * ab; - let abb = ab * b; - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; - let b_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(b))?; - let ab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(ab))?; - let aab_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(aab))?; - let abb_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(abb))?; - - a_var - .mul_no_align(&b_var)? - .enforce_congruent::(&ab_var)?; - a_var - .mul_no_align(&ab_var)? - .enforce_congruent::(&aab_var)?; - ab_var - .mul_no_align(&b_var)? - .enforce_congruent::(&abb_var)?; - - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_pow() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let rng = &mut test_rng(); - - let a = Fq::rand(rng); - - let a_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(a))?; - - let mut r_var = a_var.clone(); - for _ in 0..16 { - r_var = r_var.mul_no_align(&r_var)?.modulo::()?; - } - r_var = r_var.mul_no_align(&a_var)?.modulo::()?; - assert_eq!( - BigInt::from_biguint(Sign::Plus, a.pow([65537u64]).into()), - r_var.value()? - ); - assert!(cs.is_satisfied()?); - Ok(()) - } - - #[test] - fn test_vec_vec_mul() -> Result<(), Box> { - let cs = ConstraintSystem::::new_ref(); - - let len = 1000; - - let rng = &mut test_rng(); - let a = (0..len).map(|_| Fq::rand(rng)).collect::>(); - let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); - let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); - - let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; - let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; - let c_var = NonNativeUintVar::new_witness(cs.clone(), || Ok(c))?; - - let mut r_var = NonNativeUintVar::constant(BigUint::zero().into())?; - for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { - r_var = r_var.add_no_align(&a.mul_no_align(&b)?)?; - } - r_var.enforce_congruent::(&c_var)?; - println!("{}", cs.num_constraints()); - - assert!(cs.is_satisfied()?); - Ok(()) - } -} diff --git a/crates/primitives/src/algebra/group/nonnative.rs b/crates/primitives/src/algebra/group/emulated.rs similarity index 65% rename from crates/primitives/src/algebra/group/nonnative.rs rename to crates/primitives/src/algebra/group/emulated.rs index fd4a11bd3..733dc2a45 100644 --- a/crates/primitives/src/algebra/group/nonnative.rs +++ b/crates/primitives/src/algebra/group/emulated.rs @@ -1,10 +1,11 @@ use ark_ec::{short_weierstrass::SWFlags, AffineRepr}; -use ark_ff::{PrimeField, Zero}; +use ark_ff::Zero; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, eq::EqGadget, fields::fp::FpVar, prelude::Boolean, + select::CondSelectGadget, GR1CSVar, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; @@ -12,23 +13,25 @@ use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; use ark_std::borrow::Borrow; use crate::{ - algebra::{group::SonobeCurve}, + algebra::{field::emulated::EmulatedFieldVar, group::SonobeCurve}, + traits::SonobeField, transcripts::AbsorbableGadget, }; -use crate::algebra::field::nonnative2::BigIntVar; /// NonNativeAffineVar represents an elliptic curve point in Affine representation in the non-native /// field, over the constraint field. It is not intended to perform operations, but just to contain /// the affine coordinates in order to perform hash operations of the point. #[derive(Debug, Clone)] -pub struct NonNativeAffineVar { - pub x: BigIntVar, - pub y: BigIntVar, +pub struct EmulatedAffineVar { + pub x: EmulatedFieldVar, + pub y: EmulatedFieldVar, } -impl AllocVar for NonNativeAffineVar { - fn new_variable>( - cs: impl Into>, +impl AllocVar + for EmulatedAffineVar +{ + fn new_variable>( + cs: impl Into>, f: impl FnOnce() -> Result, mode: AllocationMode, ) -> Result { @@ -38,24 +41,24 @@ impl AllocVar for NonNativeAffineVar { let affine = val.borrow().into_affine(); let (x, y) = affine.xy().unwrap_or_default(); - let x = BigIntVar::new_variable(cs.clone(), || Ok(x), mode)?; - let y = BigIntVar::new_variable(cs.clone(), || Ok(y), mode)?; + let x = EmulatedFieldVar::new_variable(cs.clone(), || Ok(x), mode)?; + let y = EmulatedFieldVar::new_variable(cs.clone(), || Ok(y), mode)?; Ok(Self { x, y }) }) } } -impl GR1CSVar for NonNativeAffineVar { - type Value = C; +impl GR1CSVar for EmulatedAffineVar { + type Value = Target; - fn cs(&self) -> ConstraintSystemRef { + fn cs(&self) -> ConstraintSystemRef { self.x.cs().or(self.y.cs()) } fn value(&self) -> Result { - let x = C::BaseField::from_le_bytes_mod_order(&self.x.value()?.magnitude().to_bytes_le()); - let y = C::BaseField::from_le_bytes_mod_order(&self.y.value()?.magnitude().to_bytes_le()); + let x = self.x.value()?; + let y = self.y.value()?; // Below is a workaround to convert the `x` and `y` coordinates to a // point. This is because the `SonobeCurve` trait does not provide a // method to construct a point from `BaseField` elements. @@ -79,12 +82,12 @@ impl GR1CSVar for NonNativeAffineVar { // `unwrap` below is safe because `bytes` is constructed from the `x` // and `y` coordinates of a valid point, and these coordinates are // serialized in the same way as the `SonobeCurve` implementation. - Ok(C::deserialize_uncompressed_unchecked(&bytes[..]).unwrap()) + Ok(Target::deserialize_uncompressed_unchecked(&bytes[..]).unwrap()) } } -impl EqGadget for NonNativeAffineVar { - fn is_eq(&self, other: &Self) -> Result, SynthesisError> { +impl EqGadget for EmulatedAffineVar { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { Ok(self.x.is_eq(&other.x)? & self.y.is_eq(&other.y)?) } @@ -95,20 +98,37 @@ impl EqGadget for NonNativeAffineVar { } } -impl NonNativeAffineVar { +impl EmulatedAffineVar { pub fn zero() -> Self { // `unwrap` below is safe because we are allocating a constant value, // which is guaranteed to succeed. - Self::new_constant(ConstraintSystemRef::None, C::zero()).unwrap() + Self::new_constant(ConstraintSystemRef::None, Target::zero()).unwrap() } } -impl AbsorbableGadget> for NonNativeAffineVar { - fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { +impl AbsorbableGadget + for EmulatedAffineVar +{ + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { (&self.x, &self.y).absorb_into(dest) } } +impl CondSelectGadget + for EmulatedAffineVar +{ + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + Ok(Self { + x: cond.select(&true_value.x, &false_value.x)?, + y: cond.select(&true_value.y, &false_value.y)?, + }) + } +} + #[cfg(test)] mod tests { use ark_pallas::{Fq, Fr, PallasConfig, Projective}; @@ -116,20 +136,19 @@ mod tests { use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, UniformRand}; + use super::*; use crate::{ - traits::{Inputize, InputizeNonNative}, + traits::{Inputize, InputizeEmulated}, transcripts::Absorbable, }; - use super::*; - #[test] fn test_alloc_zero() { let cs = ConstraintSystem::::new_ref(); // dealing with the 'zero' point should not panic when doing the unwrap let p = Projective::zero(); - assert!(NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p)).is_ok()); + assert!(EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p)).is_ok()); } #[test] @@ -139,7 +158,7 @@ mod tests { // check that point_to_nonnative_limbs returns the expected values let mut rng = ark_std::test_rng(); let p = Projective::rand(&mut rng); - let p_var = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; + let p_var = EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p))?; assert_eq!(p_var.to_absorbable()?.value()?, p.to_absorbable()); Ok(()) } @@ -151,10 +170,10 @@ mod tests { let p = Projective::rand(&mut rng); let cs = ConstraintSystem::::new_ref(); - let p_var = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p))?; + let p_var = EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p))?; assert_eq!( [p_var.x.limbs.value()?, p_var.y.limbs.value()?].concat(), - p.inputize_nonnative() + p.inputize_emulated() ); let cs = ConstraintSystem::::new_ref(); diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs index 3f4b0fb34..2dbf59ae5 100644 --- a/crates/primitives/src/algebra/group/mod.rs +++ b/crates/primitives/src/algebra/group/mod.rs @@ -2,28 +2,24 @@ use ark_ec::{ short_weierstrass::{Projective, SWCurveConfig}, AffineRepr, CurveGroup, PrimeGroup, }; -use ark_ff::{BigInteger, Field, One, PrimeField, Zero}; -use ark_r1cs_std::convert::ToBitsGadget; +use ark_ff::{Field, One, PrimeField, Zero}; use ark_r1cs_std::{ - alloc::AllocVar, convert::ToConstraintFieldGadget, fields::fp::FpVar, groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, - prelude::Boolean, - GR1CSVar, }; use ark_relations::gr1cs::SynthesisError; use ark_std::mem::swap; -use num_bigint::{BigInt, Sign}; +use num_bigint::BigInt; use num_integer::Integer; use crate::{ - algebra::field::SonobeField, - traits::{Inputize, InputizeNonNative}, + algebra::{field::SonobeField, group::emulated::EmulatedAffineVar, Val}, + traits::{Dummy, Inputize, InputizeEmulated}, transcripts::{Absorbable, AbsorbableGadget}, }; -pub mod nonnative; +pub mod emulated; pub type CF1 = ::ScalarField; pub type CF2 = <::BaseField as Field>::BasePrimeField; @@ -33,30 +29,41 @@ pub type CI2 = <<::BaseField as Field>::BasePrimeField as Pr /// `Curve` trait is a wrapper around `CurveGroup` that also includes the /// necessary bounds for the curve to be used conveniently in folding schemes. pub trait SonobeCurve: - CurveGroup - + Absorbable + CurveGroup + + Absorbable + Inputize - + InputizeNonNative + + InputizeEmulated + + Val + AbsorbableGadget> { - /// The in-circuit variable type for this curve. - type Var: CurveVar; } impl> SonobeCurve for Projective

{ +} + +impl> Val for Projective

{ + type ConstraintField = P::BaseField; type Var = ProjectiveVar>; + + type EmulatedVar = EmulatedAffineVar; +} + +impl Dummy for C { + fn dummy(_: T) -> Self { + Default::default() + } } -impl>> Absorbable for Projective

{ - fn absorb_into(&self, dest: &mut Vec) { +impl> Absorbable for Projective

{ + fn absorb_into(&self, dest: &mut Vec) { let affine = self.into_affine(); let (x, y) = affine.xy().unwrap_or_default(); [x, y].absorb_into(dest); } } -impl> AbsorbableGadget> +impl> AbsorbableGadget for ProjectiveVar> { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { @@ -87,15 +94,15 @@ impl> Inputize for Projec } impl> - InputizeNonNative for Projective

+ InputizeEmulated for Projective

{ /// Returns the internal representation in the same order as how the value /// is allocated in `NonNativeAffineVar::new_input`. - fn inputize_nonnative(&self) -> Vec { + fn inputize_emulated(&self) -> Vec { let affine = self.into_affine(); let (x, y) = affine.xy().unwrap_or_default(); - [x, y].inputize_nonnative() + [x, y].inputize_emulated() } } @@ -128,43 +135,39 @@ fn lattice_reduction_2x2( (b1, b2) } -pub trait PointScalarMulGadget: Sized { - fn mul_scalar(&self, scalar: &impl ToBitsGadget) -> Result; -} - -impl PointScalarMulGadget> for C { - fn mul_scalar(&self, scalar: &impl ToBitsGadget>) -> Result { - let scalar = scalar.to_bits_le()?; - - let cs = scalar.cs(); - - let m = BigInt::from_biguint(Sign::Plus, CF1::::MODULUS.into()); - let m_sqrt = m.sqrt(); - - let (a, b) = lattice_reduction_2x2( - (m, Zero::zero()), - ( - CI2::::from_bits_le(&scalar.value().unwrap_or_default()) - .into() - .into(), - One::one(), - ), - ) - .0; - let (a_sign, a_abs) = a.into_parts(); - let (b_sign, b_abs) = b.into_parts(); - let a_is_negative = - Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(a_sign == Sign::Minus))?; - let b_is_negative = - Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(b_sign == Sign::Minus))?; - - // let a = NonNativeUintVar::new_variable_with_inferred_mode(cs.clone(), || { - // Ok((a_abs.into(), Bound::new_ub(m_sqrt.clone()))) - // })?; - // let b = NonNativeUintVar::new_variable_with_inferred_mode(cs, || { - // Ok((b_abs.into(), Bound::new_ub(m_sqrt))) - // })?; - - todo!() - } -} +// impl PointScalarMulGadget> for C { +// fn mul_scalar(&self, scalar: &impl ToBitsGadget>) -> Result { +// let scalar = scalar.to_bits_le()?; + +// let cs = scalar.cs(); + +// let m = BigInt::from_biguint(Sign::Plus, CF1::::MODULUS.into()); +// let m_sqrt = m.sqrt(); + +// let (a, b) = lattice_reduction_2x2( +// (m, Zero::zero()), +// ( +// CI2::::from_bits_le(&scalar.value().unwrap_or_default()) +// .into() +// .into(), +// One::one(), +// ), +// ) +// .0; +// let (a_sign, a_abs) = a.into_parts(); +// let (b_sign, b_abs) = b.into_parts(); +// let a_is_negative = +// Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(a_sign == Sign::Minus))?; +// let b_is_negative = +// Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(b_sign == Sign::Minus))?; + +// // let a = NonNativeUintVar::new_variable_with_inferred_mode(cs.clone(), || { +// // Ok((a_abs.into(), Bound::new_ub(m_sqrt.clone()))) +// // })?; +// // let b = NonNativeUintVar::new_variable_with_inferred_mode(cs, || { +// // Ok((b_abs.into(), Bound::new_ub(m_sqrt))) +// // })?; + +// todo!() +// } +// } diff --git a/crates/primitives/src/algebra/mod.rs b/crates/primitives/src/algebra/mod.rs index 82d6047fb..0a9d623dd 100644 --- a/crates/primitives/src/algebra/mod.rs +++ b/crates/primitives/src/algebra/mod.rs @@ -1,3 +1,18 @@ +use ark_ff::PrimeField; +use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; + +use crate::traits::SonobeField; + pub mod field; pub mod group; pub mod ops; + +pub trait Val { + type ConstraintField: PrimeField; + type Var: AllocVar + GR1CSVar; + + type EmulatedVar: AllocVar + GR1CSVar; +} + +pub type Var = ::Var; +pub type EmulatedVar = ::EmulatedVar; \ No newline at end of file diff --git a/crates/primitives/src/algebra/ops/bits.rs b/crates/primitives/src/algebra/ops/bits.rs index 1688a227c..1e431730b 100644 --- a/crates/primitives/src/algebra/ops/bits.rs +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -2,8 +2,10 @@ use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{alloc::AllocVar, boolean::Boolean, eq::EqGadget, fields::fp::FpVar, GR1CSVar}; use ark_relations::gr1cs::SynthesisError; +use crate::algebra::field::emulated::Bound; + pub trait FromBitsGadget: Sized { - fn from_bits_le(bits: &[Boolean]) -> Result; + fn from_bits_le(bits: &[Boolean], bound: Bound) -> Result; } pub trait ToBitsGadgetExt: Sized { @@ -15,7 +17,7 @@ pub trait ToBitsGadgetExt: Sized { } } impl FromBitsGadget for FpVar { - fn from_bits_le(bits: &[Boolean]) -> Result { + fn from_bits_le(bits: &[Boolean], _bound: Bound) -> Result { Boolean::le_bits_to_fp(bits) } } diff --git a/crates/primitives/src/algebra/ops/mod.rs b/crates/primitives/src/algebra/ops/mod.rs index 2646ce8c0..a0f820298 100644 --- a/crates/primitives/src/algebra/ops/mod.rs +++ b/crates/primitives/src/algebra/ops/mod.rs @@ -1,5 +1,6 @@ pub mod bits; pub mod eq; pub mod matrix; +pub mod pow; pub mod rlc; pub mod vector; diff --git a/crates/primitives/src/algebra/ops/pow.rs b/crates/primitives/src/algebra/ops/pow.rs new file mode 100644 index 000000000..0406ba855 --- /dev/null +++ b/crates/primitives/src/algebra/ops/pow.rs @@ -0,0 +1,30 @@ +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; + +pub trait Pow: Sized { + fn powers(&self, n: usize) -> Vec; +} + +impl Pow for F { + fn powers(&self, n: usize) -> Vec { + let mut res = vec![F::one(); n]; + for i in 1..n { + res[i] = res[i - 1] * self; + } + res + } +} + +pub trait PowGadget: Sized { + fn powers(&self, n: usize) -> Vec; +} + +impl PowGadget for FpVar { + fn powers(&self, n: usize) -> Vec { + let mut res = vec![FpVar::one(); n]; + for i in 1..n { + res[i] = &res[i - 1] * self; + } + res + } +} diff --git a/crates/primitives/src/algebra/ops/rlc.rs b/crates/primitives/src/algebra/ops/rlc.rs index 3c94d01f6..3e43ade12 100644 --- a/crates/primitives/src/algebra/ops/rlc.rs +++ b/crates/primitives/src/algebra/ops/rlc.rs @@ -26,15 +26,17 @@ pub trait SliceRLC { fn slice_rlc(self, coeffs: &[Coeff]) -> Vec; } -impl<'a, T: 'a, I: Iterator, Coeff> SliceRLC for I +impl<'a, T, I: Iterator, Coeff> SliceRLC for I where - T: Add + Copy, + T: 'a + Add + Clone, for<'x> T: Mul<&'x Coeff, Output = T>, { type Value = T; fn slice_rlc(self, coeffs: &[Coeff]) -> Vec { - let mut iter = self.zip(coeffs).map(|(v, c)| v.iter().map(|x| *x * c)); + let mut iter = self + .zip(coeffs) + .map(|(v, c)| v.iter().map(|x| x.clone() * c)); let first = iter.next().unwrap(); iter.fold(first.collect(), |acc, v| { diff --git a/crates/primitives/src/arithmetizations/ccs/circuits.rs b/crates/primitives/src/arithmetizations/ccs/circuits.rs index 6877f48d9..2abb1271f 100644 --- a/crates/primitives/src/arithmetizations/ccs/circuits.rs +++ b/crates/primitives/src/arithmetizations/ccs/circuits.rs @@ -6,19 +6,19 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::borrow::Borrow; +use super::{CCSVariant, CCS}; use crate::algebra::ops::matrix::SparseMatrixVar; -use super::CCS; - /// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. +#[allow(non_snake_case)] #[derive(Debug, Clone)] pub struct CCSMatricesVar { // we only need native representation, so the constraint field==F pub M: Vec>>, } -impl AllocVar, F> for CCSMatricesVar { - fn new_variable>>( +impl AllocVar, F> for CCSMatricesVar { + fn new_variable>>( cs: impl Into>, f: impl FnOnce() -> Result, _mode: AllocationMode, @@ -30,7 +30,7 @@ impl AllocVar, F> for CCSMatricesVar { .borrow() .M .iter() - .map(|M| SparseMatrixVar::>::new_constant(cs.clone(), M.clone())) + .map(|m| SparseMatrixVar::new_constant(cs.clone(), m)) .collect::>()?, }) }) diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index ac97cb9f7..dee3af2de 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -1,42 +1,114 @@ +use std::fmt::Debug; + use ark_ff::Field; use ark_poly::DenseMultilinearExtension; -use ark_relations::gr1cs::Matrix; -use ark_std::{cfg_into_iter, cfg_iter, log2}; +use ark_relations::gr1cs::{ConstraintSystem, Matrix}; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, log2, marker::PhantomData}; #[cfg(feature = "parallel")] use rayon::prelude::*; -use crate::circuits::Assignments; - use super::{r1cs::R1CS, Arith, ArithRelation, Error}; +use crate::{ + arithmetizations::{r1cs::R1CSConfig, ArithConfig}, + circuits::Assignments, +}; pub mod circuits; -/// CCS represents the Customizable Constraint Systems structure defined in -/// the [CCS paper](https://eprint.iacr.org/2023/552) -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CCS { +pub trait CCSVariant: Clone + Debug + PartialEq + Sync { + fn n_matrices() -> usize; + + fn degree() -> usize; + + fn multisets_vec() -> Vec>; + + fn coefficients_vec() -> Vec; +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, PartialEq)] +pub struct CCSConfig { + _v: PhantomData, /// m: number of rows in M_i (such that M_i \in F^{m, n}) - pub m: usize, + m: usize, /// n = |z|, number of cols in M_i - pub n: usize, + n: usize, /// l = |io|, size of public input/output - pub l: usize, - /// t = |M|, number of matrices - pub t: usize, - /// d: max degree in each variable - pub d: usize, - /// s = log(m), dimension of x - pub s: usize, + l: usize, +} + +impl ArithConfig for CCSConfig { + #[inline] + fn empty() -> Self { + Self { + _v: PhantomData, + m: 0, + n: 0, + l: 0, + } + } + + #[inline] + fn degree(&self) -> usize { + V::degree() + } + + #[inline] + fn n_constraints(&self) -> usize { + self.m + } + + #[inline] + fn n_variables(&self) -> usize { + self.n + } + + #[inline] + fn n_public_inputs(&self) -> usize { + self.l + } + + #[inline] + fn n_witnesses(&self) -> usize { + self.n_variables() - self.n_public_inputs() - 1 + } + + #[inline] + fn set_n_public_inputs(&mut self, l: usize) { + self.l = l; + } +} + +impl, V: CCSVariant> From for CCSConfig { + fn from(cfg: Cfg) -> Self { + let cfg = cfg.borrow(); + Self { + _v: PhantomData, + m: cfg.n_constraints(), + n: cfg.n_variables(), + l: cfg.n_public_inputs(), + } + } +} + +impl From<&ConstraintSystem> for CCSConfig { + fn from(cs: &ConstraintSystem) -> Self { + R1CSConfig::from(cs).into() + } +} + +/// CCS represents the Customizable Constraint Systems structure defined in +/// the [CCS paper](https://eprint.iacr.org/2023/552) +#[allow(non_snake_case)] +#[derive(Clone)] +pub struct CCS { + cfg: CCSConfig, /// vector of matrices pub M: Vec>, - /// vector of multisets - pub S: Vec>, - /// vector of coefficients - pub c: Vec, } -impl CCS { +impl CCS { /// Evaluates the CCS relation at a given vector of assignments `z` pub fn eval_assignments( &self, @@ -55,6 +127,9 @@ impl CCS { )); } + let S = &V::multisets_vec(); + let c = &V::coefficients_vec::(); + // Recall that the evaluation of CCS at z is defined as: // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i * z))$, // where $\prod$ denotes the Hadamard product. @@ -64,15 +139,14 @@ impl CCS { // Specifically, we independently compute each entry of the resulting // vector, and collect them at the end. // We parallelize the outer loop over rows (when the `parallel` feature - // is enabled), because `m`, the number of constraints in the CCS, is - // typically large in practice. - Ok(cfg_into_iter!(0..self.m) + // is enabled), since the number of constraints in the CCS is typically + // large in practice. + Ok(cfg_into_iter!(0..self.n_constraints()) .map(|row| { // The row-th entry of the resulting vector is: // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i[row] * z))$ - self.S - .iter() - .zip(&self.c) + S.iter() + .zip(c) .map(|(s, &c)| { // Each term in the sum is: // $c_j * \prod_{i \in S_j} (M_i[row] * z)$ @@ -93,49 +167,46 @@ impl CCS { .collect()) } - pub fn mle( + pub fn mles( &self, - i: usize, z: Assignments + Sync>, - ) -> DenseMultilinearExtension { - DenseMultilinearExtension { - num_vars: self.s, - evaluations: cfg_iter!(self.M[i]) - .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) - .chain(vec![F::zero(); (1 << self.s) - self.m]) - .collect(), - } + ) -> Vec> { + let s = log2(self.n_constraints()) as usize; + (0..V::n_matrices()) + .map(|i| DenseMultilinearExtension { + num_vars: s, + evaluations: cfg_iter!(self.M[i]) + .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) + .chain(vec![F::zero(); (1 << s) - self.n_constraints()]) + .collect(), + }) + .collect() } } -impl Arith for CCS { - #[inline] - fn degree(&self) -> usize { - self.d - } - - #[inline] - fn n_constraints(&self) -> usize { - self.m - } +impl Arith for CCS { + type Config = CCSConfig; #[inline] - fn n_variables(&self) -> usize { - self.n + fn empty() -> Self { + Self { + cfg: CCSConfig::empty(), + M: vec![vec![]; V::n_matrices()], + } } #[inline] - fn n_public_inputs(&self) -> usize { - self.l + fn config(&self) -> &Self::Config { + &self.cfg } #[inline] - fn n_witnesses(&self) -> usize { - self.n_variables() - self.n_public_inputs() - 1 + fn config_mut(&mut self) -> &mut Self::Config { + &mut self.cfg } } -impl, U: AsRef<[F]>> ArithRelation for CCS { +impl, U: AsRef<[F]>, V: CCSVariant> ArithRelation for CCS { type Evaluation = Vec; fn eval_relation(&self, w: &W, u: &U) -> Result { @@ -152,23 +223,25 @@ impl, U: AsRef<[F]>> ArithRelation for CCS { } } -impl From> for CCS { +impl From> for CCS { fn from(r1cs: R1CS) -> Self { - let m = r1cs.n_constraints(); - let n = r1cs.n_variables(); - CCS { - m, - n, - l: r1cs.n_public_inputs(), - s: log2(m) as usize, - t: 3, - d: r1cs.degree(), - - S: vec![vec![0, 1], vec![2]], - c: vec![F::one(), F::one().neg()], + Self { + cfg: r1cs.config().into(), M: vec![r1cs.A, r1cs.B, r1cs.C], } } } +impl From<&ConstraintSystem> for CCS { + fn from(cs: &ConstraintSystem) -> Self { + R1CS::from(cs).into() + } +} + +impl From> for CCS { + fn from(cs: ConstraintSystem) -> Self { + Self::from(&cs) + } +} + // TODO: add back tests diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index 01a51d8a8..28073b34d 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use ark_relations::gr1cs::SynthesisError; use thiserror::Error; @@ -14,13 +16,13 @@ pub enum Error { UnsatisfiedAssignments(String), #[error("Failed to extract constraints from the constraint system: {0}")] ConstraintExtractionFailure(String), - #[error("Synthesis error: {0}")] + #[error(transparent)] SynthesisError(#[from] SynthesisError), } -/// [`Arith`] is a trait about constraint systems (R1CS, CCS, etc.), where we -/// define methods for getting information about the constraint system. -pub trait Arith: Clone { +pub trait ArithConfig: Clone + Debug + PartialEq { + fn empty() -> Self; + /// Returns the degree of the constraint system fn degree(&self) -> usize; @@ -36,6 +38,46 @@ pub trait Arith: Clone { /// Returns the number of witnesses / secret inputs in the constraint system fn n_witnesses(&self) -> usize; + + fn set_n_public_inputs(&mut self, l: usize); +} + +/// [`Arith`] is a trait about constraint systems (R1CS, CCS, etc.), where we +/// define methods for getting information about the constraint system. +pub trait Arith: Clone { + type Config: ArithConfig; + + fn config(&self) -> &Self::Config; + + fn config_mut(&mut self) -> &mut Self::Config; + + fn empty() -> Self; + + /// Returns the degree of the constraint system + fn degree(&self) -> usize { + self.config().degree() + } + + /// Returns the number of constraints in the constraint system + fn n_constraints(&self) -> usize { + self.config().n_constraints() + } + + /// Returns the number of variables in the constraint system + fn n_variables(&self) -> usize { + self.config().n_variables() + } + + /// Returns the number of public inputs / public IO / instances / statements + /// in the constraint system + fn n_public_inputs(&self) -> usize { + self.config().n_public_inputs() + } + + /// Returns the number of witnesses / secret inputs in the constraint system + fn n_witnesses(&self) -> usize { + self.config().n_witnesses() + } } /// `ArithRelation` *treats a constraint system as a relation* between a witness diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index afeec6272..9469f2407 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -3,6 +3,7 @@ use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::{borrow::Borrow, marker::PhantomData, One}; +use super::R1CS; use crate::{ algebra::ops::{ eq::EquivalenceGadget, @@ -13,12 +14,11 @@ use crate::{ circuits::Assignments, }; -use super::R1CS; - /// An in-circuit representation of the `R1CS` struct. /// /// `M` is for the modulo operation involved in the satisfiability check when /// the underlying `FVar` is `NonNativeUintVar`. +#[allow(non_snake_case)] #[derive(Debug, Clone)] pub struct R1CSMatricesVar { _m: PhantomData, @@ -53,6 +53,7 @@ where SparseMatrixVar: MatrixGadget, [FVar]: VectorGadget, { + #[allow(non_snake_case)] pub fn eval_assignments( &self, z: Assignments>, diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 00a75740c..2733a90fa 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -1,25 +1,105 @@ use ark_ff::Field; -use ark_relations::gr1cs::{ConstraintSystem, Matrix}; +use ark_relations::gr1cs::{ConstraintSystem, Matrix, R1CS_PREDICATE_LABEL}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{cfg_into_iter, cfg_iter, iterable::Iterable}; #[cfg(feature = "parallel")] use rayon::prelude::*; +use super::{ccs::CCS, Arith, ArithRelation, Error}; use crate::{ - circuits::{Assignments, ConstraintSystemExt}, + arithmetizations::{ccs::CCSVariant, ArithConfig}, + circuits::Assignments, relations::WitnessInstanceExtractor, - traits::Dummy, }; -use super::{ccs::CCS, Arith, ArithRelation, Error}; - pub mod circuits; #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct R1CS { - l: usize, // io len +pub struct R1CSConfig { m: usize, // number of constraints n: usize, // number of variables + l: usize, // io len +} + +impl R1CSConfig { + pub fn new(n_constraints: usize, n_variables: usize, n_public_inputs: usize) -> Self { + Self { + m: n_constraints, + n: n_variables, + l: n_public_inputs, + } + } +} + +impl ArithConfig for R1CSConfig { + #[inline] + fn empty() -> Self { + Self { m: 0, n: 0, l: 0 } + } + + #[inline] + fn degree(&self) -> usize { + 2 + } + + #[inline] + fn n_constraints(&self) -> usize { + self.m + } + + #[inline] + fn n_variables(&self) -> usize { + self.n + } + + #[inline] + fn n_public_inputs(&self) -> usize { + self.l + } + + #[inline] + fn n_witnesses(&self) -> usize { + self.n_variables() - self.n_public_inputs() - 1 + } + + #[inline] + fn set_n_public_inputs(&mut self, l: usize) { + self.l = l; + } +} + +impl From<&ConstraintSystem> for R1CSConfig { + fn from(cs: &ConstraintSystem) -> Self { + Self::new( + cs.num_constraints(), + cs.num_instance_variables + cs.num_witness_variables, + cs.num_instance_variables - 1, // -1 to subtract the first '1' + ) + } +} + +impl CCSVariant for R1CSConfig { + fn n_matrices() -> usize { + 3 + } + + fn degree() -> usize { + 2 + } + + fn multisets_vec() -> Vec> { + vec![vec![0, 1], vec![2]] + } + + fn coefficients_vec() -> Vec { + vec![F::one(), -F::one()] + } +} + +#[allow(non_snake_case)] +#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +pub struct R1CS { + cfg: R1CSConfig, pub A: Matrix, pub B: Matrix, pub C: Matrix, @@ -58,90 +138,68 @@ impl R1CS { } impl Arith for R1CS { - #[inline] - fn degree(&self) -> usize { - 2 - } - - #[inline] - fn n_constraints(&self) -> usize { - self.m - } + type Config = R1CSConfig; #[inline] - fn n_variables(&self) -> usize { - self.n + fn empty() -> Self { + Self { + cfg: R1CSConfig::empty(), + A: vec![], + B: vec![], + C: vec![], + } } #[inline] - fn n_public_inputs(&self) -> usize { - self.l + fn config(&self) -> &Self::Config { + &self.cfg } #[inline] - fn n_witnesses(&self) -> usize { - self.n_variables() - self.n_public_inputs() - 1 - } -} - -impl Dummy<(usize, usize, usize)> for R1CS { - fn dummy((n_constraints, n_variables, n_public_inputs): (usize, usize, usize)) -> Self { - Self { - m: n_constraints, - n: n_variables, - l: n_public_inputs, - A: vec![], - B: vec![], - C: vec![], - } + fn config_mut(&mut self) -> &mut Self::Config { + &mut self.cfg } } impl R1CS { - pub fn empty() -> Self { - Self::dummy((0, 0, 0)) - } - - pub fn new( - (n_constraints, n_variables, n_public_inputs): (usize, usize, usize), - matrices: [Matrix; 3], - ) -> Self { - let mut matrices = matrices.to_vec(); - let C = matrices.pop().unwrap(); - let B = matrices.pop().unwrap(); - let A = matrices.pop().unwrap(); - Self { - m: n_constraints, - n: n_variables, - l: n_public_inputs, - A, - B, - C, - } + #[allow(non_snake_case)] + pub fn new(cfg: R1CSConfig, [A, B, C]: [Matrix; 3]) -> Self { + Self { cfg, A, B, C } } } -impl TryFrom> for R1CS { +impl TryFrom> for R1CS { type Error = Error; - fn try_from(ccs: CCS) -> Result { - if ccs.t != 3 { - return Err(Error::ConstraintExtractionFailure(format!( - "R1CS should only have 3 matrices (A, B, C) but found {} matrices", - ccs.t - ))); - } + fn try_from(ccs: CCS) -> Result { Ok(Self::new( - ( + R1CSConfig::new( ccs.n_constraints(), ccs.n_variables(), ccs.n_public_inputs(), ), + // `unwrap` is safe here because the type parameter T = 3 ccs.M.try_into().unwrap(), )) } } +impl From<&ConstraintSystem> for R1CS { + fn from(cs: &ConstraintSystem) -> Self { + // Get the R1CS predicate matrices + let r1cs_predicate = &cs.predicate_constraint_systems[R1CS_PREDICATE_LABEL]; + let matrices = r1cs_predicate.to_matrices(cs); + // `unwrap` is safe here because R1CS always has 3 matrices + R1CS::new(cs.into(), matrices.try_into().unwrap()) + } +} + +impl From> for R1CS { + fn from(cs: ConstraintSystem) -> Self { + Self::from(&cs) + } +} + impl, U: AsRef<[F]>> ArithRelation for R1CS { type Evaluation = Vec; @@ -220,46 +278,6 @@ impl WitnessInstanceExtractor>, RelaxedInstance< } } -impl ConstraintSystemExt for ConstraintSystem { - type Arith = R1CS; - type Error = Error; - - fn constraints(&self) -> Result, Error> { - // Get the R1CS predicate matrices - let r1cs_predicate = self - .predicate_constraint_systems - .get("R1CS") - .ok_or_else(|| { - Error::ConstraintExtractionFailure( - "No R1CS predicate found in constraint system".into(), - ) - })?; - let matrices = r1cs_predicate.to_matrices(self); - if matrices.len() != 3 { - return Err(Error::ConstraintExtractionFailure(format!( - "R1CS should only have 3 matrices (A, B, C) but found {} matrices", - matrices.len() - ))); - } - Ok(R1CS::new( - ( - self.num_constraints(), - self.num_instance_variables + self.num_witness_variables, - self.num_instance_variables - 1, // -1 to subtract the first '1' - ), - matrices.try_into().unwrap(), - )) - } - - fn assignments(&self) -> Result>, Error> { - let witness = self.witness_assignment()?.to_vec(); - // skip the first element which is '1' - let instance = self.instance_assignment()?[1..].to_vec(); - - Ok((F::one(), instance, witness).into()) - } -} - #[cfg(test)] pub mod tests { use ark_bn254::Fr; @@ -267,11 +285,11 @@ pub mod tests { use ark_relations::gr1cs::ConstraintSynthesizer; use ark_std::{error::Error, test_rng}; - use crate::circuits::utils::{ - constraints_for_test, satisfying_assignments_for_test, CircuitForTest, - }; - use super::*; + use crate::circuits::{ + utils::{constraints_for_test, satisfying_assignments_for_test, CircuitForTest}, + ConstraintSystemExt, + }; #[test] fn test_constraint_extraction() -> Result<(), Box> { @@ -285,7 +303,7 @@ pub mod tests { cs.finalize(); let cs = cs.into_inner().unwrap(); - assert_eq!(cs.constraints()?, constraints_for_test()); + assert_eq!(R1CS::from(&cs), constraints_for_test()); Ok(()) } diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index 1e8f34fb8..c38edbd69 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use ark_ff::{Field, PrimeField}; use ark_r1cs_std::fields::fp::FpVar; use ark_relations::gr1cs::{ @@ -22,6 +24,8 @@ pub trait FCircuit { type Field: PrimeField; type ExternalInputs; + fn dummy_external_inputs(&self) -> Self::ExternalInputs; + /// returns the number of elements in the state of the FCircuit, which corresponds to the /// FCircuit inputs. fn state_len(&self) -> usize; @@ -32,7 +36,7 @@ pub trait FCircuit { // can hold a state if needed to store data to generate the constraints. &self, cs: ConstraintSystemRef, - i: usize, + i: FpVar, z_i: Vec>, external_inputs: Self::ExternalInputs, // inputs that are not part of the state ) -> Result>, SynthesisError>; @@ -155,11 +159,16 @@ impl> ConstraintSystemBuilder { } } -pub trait ConstraintSystemExt: Sized { - type Error; - type Arith; +pub trait ConstraintSystemExt { + fn assignments(&self) -> Result>, SynthesisError>; +} - fn constraints(&self) -> Result; +impl ConstraintSystemExt for ConstraintSystem { + fn assignments(&self) -> Result>, SynthesisError> { + let witness = self.witness_assignment()?.to_vec(); + // skip the first element which is '1' + let instance = self.instance_assignment()?[1..].to_vec(); - fn assignments(&self) -> Result>, Self::Error>; + Ok((F::one(), instance, witness).into()) + } } diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs index 2b01cf1ba..304c2119f 100644 --- a/crates/primitives/src/circuits/utils.rs +++ b/crates/primitives/src/circuits/utils.rs @@ -1,14 +1,20 @@ use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::{alloc::AllocVar, fields::fp::AllocatedFp}; +use ark_r1cs_std::{ + alloc::AllocVar, + fields::fp::{AllocatedFp, FpVar}, +}; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError, Variable}; -use crate::arithmetizations::r1cs::R1CS; - use super::Assignments; +use crate::{ + arithmetizations::r1cs::{R1CSConfig, R1CS}, + circuits::FCircuit, +}; pub struct CircuitForTest { pub x: F, } + impl ConstraintSynthesizer for CircuitForTest { fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { // Variable 0 (implicitly added by arkworks as 1) @@ -41,6 +47,61 @@ impl ConstraintSynthesizer for CircuitForTest { } } +impl FCircuit for CircuitForTest { + type Field = F; + + type ExternalInputs = (); + + fn dummy_external_inputs(&self) -> Self::ExternalInputs {} + + fn state_len(&self) -> usize { + 1 + } + + fn generate_step_constraints( + &self, + cs: ConstraintSystemRef, + _i: FpVar, + z_i: Vec>, + _external_inputs: Self::ExternalInputs, + ) -> Result>, SynthesisError> { + // Variable 0 (implicitly added by arkworks as 1) + // Variable 1 + let x = if let FpVar::Var(x) = z_i[0].clone() { + x + } else { + unreachable!() + }; + // Variable 2 + let y = AllocatedFp::new_witness(cs.clone(), || { + Ok(x.value()?.pow([3]) + x.value()? + F::from(5)) + })?; + + // Variable 3, Constraint 0 + let x_square = x.square()?; + // Variable 4, Constraint 1 + let x_cube = x_square.mul(&x); + // Variable 5 + let t = AllocatedFp::new_witness(cs.clone(), || Ok(x.value()?.pow([3]) + x.value()?))?; + let x_cube_plus_x = x.add(&x_cube); + // Constraint 2 + cs.enforce_r1cs_constraint( + || x_cube_plus_x.variable.into(), + || Variable::one().into(), + || t.variable.into(), + )?; + let x_cube_plus_x_plus_5 = t.add_constant(F::from(5)); + // Constraint 3 + cs.enforce_r1cs_constraint( + || x_cube_plus_x_plus_5.variable.into(), + || Variable::one().into(), + || y.variable.into(), + )?; + Ok(vec![FpVar::Var(x_cube_plus_x_plus_5)]) + } +} + +#[allow(non_snake_case)] pub fn constraints_for_test() -> R1CS { // R1CS for: x^3 + x + 5 = y (example from article // https://vitalik.eth.limo/general/2016/12/10/qap.html) @@ -63,7 +124,7 @@ pub fn constraints_for_test() -> R1CS { vec![(F::one(), 2)], ]; - R1CS::::new((4, 6, 1), [A, B, C]) + R1CS::::new(R1CSConfig::new(4, 6, 1), [A, B, C]) } pub fn satisfying_assignments_for_test(x: F) -> Assignments> { diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index a8b1a9b13..110a633f6 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,8 +1,8 @@ -use ark_ff::Field; -use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_r1cs_std::{ + alloc::AllocVar, eq::EqGadget, fields::fp::FpVar, select::CondSelectGadget, GR1CSVar, +}; +use ark_relations::gr1cs::SynthesisError; use ark_std::{ - borrow::Borrow, fmt::Debug, iter::Sum, ops::{Add, Mul}, @@ -10,14 +10,22 @@ use ark_std::{ }; use thiserror::Error; +use crate::{ + algebra::{ + field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar, + ops::bits::FromBitsGadget, Var, + }, + traits::{SonobeCurve, SonobeField, CF1, CF2}, + transcripts::{Absorbable, AbsorbableGadget}, +}; + pub mod pedersen; // TODO: add back other commitment schemes #[derive(Debug, Error)] pub enum Error { // Commitment errors - #[error("The message being committed to has length {1}, exceeding the maximum supported length of {0}" - )] + #[error("The message being committed to has length {1}, exceeding the maximum supported length ({0})")] MessageTooLong(usize, usize), #[error("Blinding factor not 0 for Commitment without hiding")] BlindingNotZero, @@ -27,16 +35,20 @@ pub enum Error { CommitmentVerificationFail, } -pub trait VectorCommitment: 'static + Debug + PartialEq { +pub trait VectorCommitment: 'static + Clone + Debug + PartialEq + Eq { const IS_HIDING: bool; - type Key; - type Scalar: Clone + Copy + Debug + PartialEq + Sync; - type Commitment: Default + Debug + PartialEq + Sync; + type Gadget: VectorCommitmentGadget; + + type Key: Clone; + type Scalar: Clone + Copy + Default + Debug + PartialEq + Eq + Sync + Absorbable; + type Commitment: Clone + Default + Debug + PartialEq + Eq + Sync + Absorbable; type Randomness: Clone + Copy + Default + Debug + + PartialEq + + Eq + Sync + Add + Mul @@ -62,13 +74,38 @@ pub trait VectorCommitment: 'static + Debug + PartialEq { ) -> Result; } -pub trait VectorCommitmentGadget { +pub trait GroupBasedVectorCommitment: + VectorCommitment< + Gadget: VectorCommitmentGadget< + Native = Self, + ConstraintField = CF2, + ScalarVar = EmulatedFieldVar, Self::Scalar, true>, + CommitmentVar = Var, + >, + Commitment: SonobeCurve, + Scalar = CF1<::Commitment>, +> +{ + type EmulatedGadget: VectorCommitmentGadget< + Native = Self, + ConstraintField = Self::Scalar, + ScalarVar = FpVar, + CommitmentVar = EmulatedAffineVar, + >; +} + +pub trait VectorCommitmentGadget: Clone { type Native: VectorCommitment; - type ConstraintField: Field; + type ConstraintField: SonobeField; type KeyVar; type ScalarVar: Clone + + EqGadget + + AbsorbableGadget + + CondSelectGadget + + FromBitsGadget + AllocVar<::Scalar, Self::ConstraintField> + + GR1CSVar::Scalar> + Add + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> + Mul @@ -84,11 +121,12 @@ pub trait VectorCommitmentGadget { + Mul + for<'a> Mul<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar>; type CommitmentVar: Clone - + AllocVar<::Commitment, Self::ConstraintField>; - type RandomnessVar: AllocVar< - ::Randomness, - Self::ConstraintField, - >; + + AbsorbableGadget + + CondSelectGadget + + AllocVar<::Commitment, Self::ConstraintField> + + GR1CSVar::Commitment>; + type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> + + GR1CSVar::Randomness>; fn open( ck: &Self::KeyVar, @@ -98,57 +136,6 @@ pub trait VectorCommitmentGadget { ) -> Result<(), SynthesisError>; } -#[derive(Clone, Copy, Default, Debug)] -pub struct Null; - -impl Add for Null { - type Output = Null; - - fn add(self, _: F) -> Null { - Null - } -} - -impl Add for &Null { - type Output = Null; - - fn add(self, _: F) -> Null { - Null - } -} - -impl Mul for Null { - type Output = Self; - - fn mul(self, _: F) -> Null { - Null - } -} - -impl Mul for &Null { - type Output = Null; - - fn mul(self, _: F) -> Null { - Null - } -} - -impl Sum for Null { - fn sum>(_: I) -> Self { - Null - } -} - -impl AllocVar for Null { - fn new_variable>( - _cs: impl Into>, - _f: impl FnOnce() -> Result, - _mode: AllocationMode, - ) -> Result { - Ok(Self) - } -} - #[cfg(test)] mod tests { use ark_ff::UniformRand; diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index bac75c6e8..4eb08956d 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -1,31 +1,18 @@ -use ark_ec::{ - short_weierstrass::{Projective, SWCurveConfig}, - CurveGroup, -}; -use ark_ff::{AdditiveGroup, PrimeField}; use ark_r1cs_std::{ - boolean::Boolean, - convert::ToBitsGadget, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - groups::{ - curves::short_weierstrass::{non_zero_affine::NonZeroAffineVar, ProjectiveVar}, - CurveVar, - }, - GR1CSVar, + boolean::Boolean, convert::ToBitsGadget, eq::EqGadget, fields::fp::FpVar, groups::CurveVar, }; use ark_relations::gr1cs::SynthesisError; use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; use super::{Error, VectorCommitment}; -use crate::traits::CF1; use crate::{ - algebra::field::nonnative2::NonNativeFieldVar, - commitments::{Null, VectorCommitmentGadget}, - traits::{SonobeCurve, CF2}, + algebra::{field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar}, + commitments::{GroupBasedVectorCommitment, VectorCommitmentGadget}, + traits::{CF1, CF2, SonobeCurve}, + utils::null::Null, }; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Pedersen { _c: PhantomData, } @@ -44,6 +31,8 @@ impl Pedersen { impl VectorCommitment for Pedersen { const IS_HIDING: bool = false; + type Gadget = PedersenGadget; + type Key = Vec; type Scalar = C::ScalarField; type Commitment = C; @@ -77,6 +66,8 @@ impl VectorCommitment for Pedersen { impl VectorCommitment for Pedersen { const IS_HIDING: bool = true; + type Gadget = PedersenGadget; + type Key = (Vec, C); type Scalar = C::ScalarField; type Commitment = C; @@ -108,6 +99,15 @@ impl VectorCommitment for Pedersen { } } +impl GroupBasedVectorCommitment for Pedersen { + type EmulatedGadget = PedersenEmulatedGadget; +} + +impl GroupBasedVectorCommitment for Pedersen { + type EmulatedGadget = PedersenEmulatedGadget; +} + +#[derive(Clone)] pub struct PedersenGadget { _c: PhantomData, } @@ -228,9 +228,9 @@ impl VectorCommitmentGadget for PedersenGadget { type KeyVar = Vec; - type ScalarVar = NonNativeFieldVar, CF1, true>; + type ScalarVar = EmulatedFieldVar, CF1, true>; - type IntermediateScalarVar = NonNativeFieldVar, CF1, false>; + type IntermediateScalarVar = EmulatedFieldVar, CF1, false>; type CommitmentVar = C::Var; @@ -258,13 +258,13 @@ impl VectorCommitmentGadget for PedersenGadget { type KeyVar = (Vec, C::Var); - type ScalarVar = NonNativeFieldVar, CF1, true>; + type ScalarVar = EmulatedFieldVar, CF1, true>; - type IntermediateScalarVar = NonNativeFieldVar, CF1, false>; + type IntermediateScalarVar = EmulatedFieldVar, CF1, false>; type CommitmentVar = C::Var; - type RandomnessVar = NonNativeFieldVar, CF1, true>; + type RandomnessVar = EmulatedFieldVar, CF1, true>; fn open( (g, h): &Self::KeyVar, @@ -283,14 +283,69 @@ impl VectorCommitmentGadget for PedersenGadget { } } +#[derive(Clone)] +pub struct PedersenEmulatedGadget { + _c: PhantomData, +} + +impl VectorCommitmentGadget for PedersenEmulatedGadget { + type Native = Pedersen; + type ConstraintField = CF1; + + type KeyVar = Vec, C>>; + + type ScalarVar = FpVar>; + + type IntermediateScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = Null; + + fn open( + ck: &Self::KeyVar, + v: &[Self::ScalarVar], + _r: &Self::RandomnessVar, + cm: &Self::CommitmentVar, + ) -> Result<(), SynthesisError> { + unimplemented!() + } +} + +impl VectorCommitmentGadget for PedersenEmulatedGadget { + type Native = Pedersen; + type ConstraintField = CF1; + + type KeyVar = ( + Vec, C>>, + EmulatedAffineVar, C>, + ); + + type ScalarVar = FpVar>; + + type IntermediateScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = FpVar>; + + fn open( + (g, h): &Self::KeyVar, + v: &[Self::ScalarVar], + r: &Self::RandomnessVar, + cm: &Self::CommitmentVar, + ) -> Result<(), SynthesisError> { + unimplemented!() + } +} + #[cfg(test)] mod tests { use ark_bn254::G1Projective; use ark_std::{error::Error, rand::Rng, test_rng}; - use crate::commitments::tests::test_commitment_correctness; - use super::*; + use crate::commitments::tests::test_commitment_correctness; #[test] fn test_pedersen_commitment() -> Result<(), Box> { diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index c91d06afb..8e76b75ab 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -6,3 +6,4 @@ pub mod relations; pub mod sumcheck; pub mod traits; pub mod transcripts; +pub mod utils; diff --git a/crates/primitives/src/sumcheck/circuits.rs b/crates/primitives/src/sumcheck/circuits.rs new file mode 100644 index 000000000..640e59725 --- /dev/null +++ b/crates/primitives/src/sumcheck/circuits.rs @@ -0,0 +1,117 @@ +/// Heavily inspired from testudo: https://github.com/cryptonetlab/testudo/tree/master +/// Some changes: +/// - Typings to better stick to ark_poly's API +/// - Uses `folding-schemes`' own `TranscriptVar` trait and `PoseidonTranscriptVar` struct +/// - API made closer to gadgets found in `folding-schemes` +use ark_ff::PrimeField; +use ark_r1cs_std::{ + eq::EqGadget, + fields::{fp::FpVar, FieldVar}, + poly::polynomial::univariate::dense::DensePolynomialVar, +}; +use ark_relations::gr1cs::SynthesisError; + +use crate::{sumcheck::utils::VPAuxInfo, transcripts::TranscriptVar}; + +pub struct IOPSumCheckGadget; + +impl IOPSumCheckGadget { + pub fn verify( + claimed_sum: FpVar, + proofs: &Vec>>, + aux_info: &VPAuxInfo, + transcript: &mut impl TranscriptVar, + ) -> Result<(FpVar, Vec>), SynthesisError> { + transcript.add(&FpVar::constant(F::from(aux_info.num_variables as u64)))?; + transcript.add(&FpVar::constant(F::from(aux_info.max_degree as u64)))?; + if proofs.len() != aux_info.num_variables { + return Err(SynthesisError::Unsatisfiable); + } + + let mut challenges = Vec::with_capacity(aux_info.num_variables); + let mut expected = claimed_sum; + + for coeffs in proofs { + if coeffs.len() - 1 != aux_info.max_degree { + return Err(SynthesisError::Unsatisfiable); + } + + let eval_at_zero = &coeffs[0]; + let eval_at_one = coeffs.iter().sum::>(); + + (eval_at_zero + eval_at_one).enforce_equal(&expected)?; + + transcript.add(&coeffs)?; + let challenge = transcript.challenge_field_element()?; + + expected = DensePolynomialVar::from_coefficients_slice(coeffs).evaluate(&challenge)?; + challenges.push(challenge); + } + + Ok((expected, challenges)) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_crypto_primitives::sponge::{ + poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, + CryptographicSponge, + }; + use ark_ff::{One, Zero}; + use ark_poly::{ + univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, + MultilinearExtension, Polynomial, + }; + use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{error::Error, test_rng}; + + use super::*; + use crate::{ + sumcheck::{utils::VirtualPolynomial, IOPSumCheck}, + transcripts::poseidon::poseidon_canonical_config, + }; + + #[test] + fn test_sum_check_circuit() -> Result<(), Box> { + let mut rng = test_rng(); + let poseidon_config = poseidon_canonical_config::(); + for num_vars in 1..15 { + let mut transcript_p = PoseidonSponge::new(&poseidon_config); + let mut transcript_v = PoseidonSponge::new(&poseidon_config); + + let poly_mle = DenseMultilinearExtension::rand(num_vars, &mut rng); + let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, One::one()); + let aux_info = virtual_poly.aux_info.clone(); + + let (proofs, challenges, _) = IOPSumCheck::prove(virtual_poly, &mut transcript_p)?; + + let poly = DensePolynomial::from_coefficients_slice(&proofs[0]); + let claimed_sum = poly.evaluate(&One::one()) + poly.evaluate(&Zero::zero()); + + let (expected, _) = + IOPSumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v)?; + + let cs = ConstraintSystem::new_ref(); + let mut transcript_var = PoseidonSpongeVar::new(&poseidon_config); + + let (expected_var, challenges_var) = IOPSumCheckGadget::verify( + FpVar::new_witness(cs.clone(), || Ok(claimed_sum))?, + &proofs + .into_iter() + .map(|v| Vec::new_witness(cs.clone(), || Ok(v))) + .collect::>()?, + &aux_info, + &mut transcript_var, + )?; + + assert!(cs.is_satisfied()?); + + assert_eq!(expected_var.value()?, expected); + assert_eq!(challenges_var.value()?, challenges); + } + Ok(()) + } +} diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index 141370f47..4b32465ad 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -13,61 +13,38 @@ use ark_ff::PrimeField; use ark_poly::{ univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, Polynomial, }; -use ark_std::{cfg_chunks, cfg_into_iter, cfg_iter, fmt::Debug}; +use ark_std::{cfg_chunks, cfg_into_iter, fmt::Debug}; #[cfg(feature = "parallel")] use rayon::prelude::*; use thiserror::Error; -use crate::transcripts::{Absorbable, Transcript}; - -use utils::{ +use self::utils::{ barycentric_weights, compute_lagrange_interpolated_poly, extrapolate, VPAuxInfo, VirtualPolynomial, }; +use crate::transcripts::{Absorbable, Transcript}; +pub mod circuits; pub mod utils; #[derive(Debug, Error)] pub enum Error { - #[error("Invalid Polynomial IOP Prover: {0}")] - InvalidPolyIOPProver(String), - #[error("Invalid Polynomial IOP Verifier: {0}")] - InvalidPolyIOPVerifier(String), - #[error("Invalid Polynomial IOP Proof: {0}")] - InvalidPolyIOPProof(String), - #[error("Invalid Polynomial IOP Parameters: {0}")] - InvalidPolyIOPParameters(String), -} - -/// An IOP proof is a collections of -/// - messages from prover to verifier at each round through the interactive -/// protocol. -/// - a point that is generated by the transcript for evaluation -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct IOPProof { - pub point: Vec, - pub proofs: Vec>, -} - -/// A SumCheckSubClaim is a claim generated by the verifier at the end of -/// verification when it is convinced. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SumCheckSubClaim { - /// the multi-dimensional point that this multilinear extension is evaluated - /// to - pub point: Vec, - /// the expected evaluation - pub expected_evaluation: F, + #[error("Incorrect evaluations: {0} + {1} != {2}")] + IncorrectEvaluations(String, String, String), + #[error("Incorrect proof length: expected {0}, got {1}")] + UnexpectedProofLength(usize, usize), + #[error("Unexpected polynomial degree: expected at most {0}, got {1}")] + UnexpectedPolynomialDegree(usize, usize), } #[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] pub struct IOPSumCheck; impl IOPSumCheck { - pub fn prove>( + pub fn prove( mut poly: VirtualPolynomial, transcript: &mut impl Transcript, - ) -> Result<(IOPProof, Vec>), Error> { + ) -> Result<(Vec>, Vec, Vec>), Error> { transcript.add(&F::from(poly.aux_info.num_variables as u64)); transcript.add(&F::from(poly.aux_info.max_degree as u64)); let extrapolation_aux = (1..poly.aux_info.max_degree) @@ -159,7 +136,8 @@ impl IOPSumCheck { .for_each(|(products_sum, sum)| *products_sum += sum); }); - let prover_poly = compute_lagrange_interpolated_poly(&products_sum).coeffs; + let mut prover_poly = compute_lagrange_interpolated_poly(&products_sum).coeffs; + prover_poly.resize(poly.aux_info.max_degree + 1, F::ZERO); transcript.add(&prover_poly); prover_msgs.push(prover_poly); @@ -173,82 +151,72 @@ impl IOPSumCheck { }); } - Ok(( - IOPProof { - point: challenges, - proofs: prover_msgs, - }, - poly.flattened_ml_extensions, - )) + Ok((prover_msgs, challenges, poly.flattened_ml_extensions)) } - pub fn verify>( + pub fn verify( claimed_sum: F, - proof: &IOPProof, + proofs: &[Vec], aux_info: &VPAuxInfo, transcript: &mut impl Transcript, - ) -> Result, Error> { + ) -> Result<(F, Vec), Error> { transcript.add(&F::from(aux_info.num_variables as u64)); transcript.add(&F::from(aux_info.max_degree as u64)); - assert_eq!(aux_info.num_variables, proof.proofs.len()); + if proofs.len() != aux_info.num_variables { + return Err(Error::UnexpectedProofLength( + aux_info.num_variables, + proofs.len(), + )); + } - let challenges = proof - .proofs - .iter() - .map(|msg| { - transcript.add(msg); - transcript.challenge_field_element() - }) - .collect::>(); + let mut challenges = Vec::with_capacity(aux_info.num_variables); + let mut expected = claimed_sum; - // the deferred check during the interactive phase: - // 2. set `expected` to P(r)` - let mut expected_vec = cfg_iter!(proof.proofs) - .zip(&challenges) - .map(|(coeffs, challenge)| { - DensePolynomial::from_coefficients_slice(coeffs).evaluate(challenge) - }) - .collect::>(); - - // insert the asserted_sum to the first position of the expected vector - expected_vec.insert(0, claimed_sum); + // Outer loop is not parallelized because `DensePolynomial::evaluate` is + // already parallelized internally. + for coeffs in proofs { + if coeffs.len() - 1 != aux_info.max_degree { + return Err(Error::UnexpectedPolynomialDegree( + aux_info.max_degree, + coeffs.len() - 1, + )); + } - for (coeffs, &expected) in proof.proofs.iter().zip(expected_vec.iter()) { - let eval_at_one: F = coeffs.iter().sum(); - let eval_at_zero: F = if coeffs.is_empty() { - F::zero() - } else { - coeffs[0] - }; + let eval_at_zero = coeffs[0]; + let eval_at_one = coeffs.iter().sum::(); // the deferred check during the interactive phase: // 1. check if the received 'P(0) + P(1) = expected`. - if eval_at_one + eval_at_zero != expected { - return Err(Error::InvalidPolyIOPProof( - "Prover message is not consistent with the claim.".to_string(), + if eval_at_zero + eval_at_one != expected { + return Err(Error::IncorrectEvaluations( + eval_at_zero.to_string(), + eval_at_one.to_string(), + expected.to_string(), )); } + + transcript.add(coeffs); + let challenge = transcript.challenge_field_element(); + + // 2. set `expected` to `P(r)` + expected = DensePolynomial::from_coefficients_slice(coeffs).evaluate(&challenge); + challenges.push(challenge); } - Ok(SumCheckSubClaim { - point: challenges, - // the last expected value (not checked within this function) will be included in the - // subclaim - expected_evaluation: expected_vec[expected_vec.len() - 1], - }) + + Ok((expected, challenges)) } } #[cfg(test)] pub mod tests { - use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge}; + use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; use ark_ff::Field; use ark_pallas::Fr; use ark_poly::{DenseMultilinearExtension, MultilinearExtension}; use ark_std::{test_rng, One, Zero}; - use crate::transcripts::poseidon::poseidon_canonical_config; - use super::*; + use crate::transcripts::poseidon::poseidon_canonical_config; #[test] pub fn sumcheck_poseidon() -> Result<(), Error> { @@ -263,7 +231,7 @@ pub mod tests { // test with zero poly let poly_mle = DenseMultilinearExtension::from_evaluations_vec( n_vars, - vec![Fr::zero(); 2u32.pow(n_vars as u32) as usize], + vec![Fr::zero(); 2usize.pow(n_vars as u32)], ); let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); sumcheck_poseidon_opt(virtual_poly)?; @@ -276,13 +244,13 @@ pub mod tests { // sum-check prove let mut transcript_p: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); - let (sum_check, _) = IOPSumCheck::prove(virtual_poly, &mut transcript_p)?; + let (proofs, _, _) = IOPSumCheck::prove(virtual_poly, &mut transcript_p)?; // sum-check verify - let poly = DensePolynomial::from_coefficients_vec(sum_check.proofs[0].clone()); + let poly = DensePolynomial::from_coefficients_slice(&proofs[0]); let claimed_sum = poly.evaluate(&Fr::one()) + poly.evaluate(&Fr::zero()); let mut transcript_v: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); - let res_verify = IOPSumCheck::verify(claimed_sum, &sum_check, &aux_info, &mut transcript_v); + let res_verify = IOPSumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v); assert!(res_verify.is_ok()); Ok(()) diff --git a/crates/primitives/src/sumcheck/utils.rs b/crates/primitives/src/sumcheck/utils.rs index d75a973b4..2cc71f748 100644 --- a/crates/primitives/src/sumcheck/utils.rs +++ b/crates/primitives/src/sumcheck/utils.rs @@ -10,8 +10,9 @@ //! This module defines our main mathematical object `VirtualPolynomial`; and //! various functions associated with it. -use ark_ff::{batch_inversion, PrimeField}; +use ark_ff::{batch_inversion, Field, PrimeField}; use ark_poly::{univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial}; +use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; use ark_serialize::CanonicalSerialize; use ark_std::cfg_into_iter; #[cfg(feature = "parallel")] @@ -57,7 +58,6 @@ pub struct VPAuxInfo { pub num_variables: usize, } -// TODO: convert this into a trait impl VirtualPolynomial { /// Creates a new virtual polynomial from a MLE and its coefficient. pub fn new_from_mle(mle: DenseMultilinearExtension, coefficient: F) -> Self { @@ -74,51 +74,67 @@ impl VirtualPolynomial { } } -/// Evaluate eq polynomial. -pub fn eq_eval(x: &[F], y: &[F]) -> F { - debug_assert_eq!(x.len(), y.len()); - x.iter() - .zip(y.iter()) - .map(|(xi, yi)| xi.double() * yi - xi - yi + F::one()) - .product::() -} - -/// This function build the eq(x, r) polynomial for any given r, and output the -/// evaluation of eq(x, r) in its vector form. +/// `EqPoly` represents the following polynomial: /// -/// Evaluate -/// eq(x,y) = \prod_i=1^num_var (x_i * y_i + (1-x_i)*(1-y_i)) -/// over r, which is -/// eq(x,y) = \prod_i=1^num_var (x_i * r_i + (1-x_i)*(1-r_i)) -pub fn build_eq_x_r_vec(r: &[F]) -> Vec { - // we build eq(x,r) from its evaluations - // we want to evaluate eq(x,r) over x \in {0, 1}^num_vars - // for example, with num_vars = 4, x is a binary vector of 4, then - // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) - // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) - // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) - // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) - // .... - // 1 1 1 1 -> r0 * r1 * r2 * r3 - // we will need 2^num_var evaluations - - // initializing the buffer with [1] - let mut buf = vec![F::one()]; - - for i in r.iter().rev() { - // suppose at the previous step we received [b_1, ..., b_k] - // for the current step we will need - // if x_i = 0: (1-ri) * [b_1, ..., b_k] - // if x_i = 1: ri * [b_1, ..., b_k] - buf = cfg_into_iter!(buf) - .flat_map(|j| { - let v = j * i; - [j - v, v] - }) - .collect(); +/// `eq(x, y) = \prod_{i=1}^n (x_i * y_i + (1 - x_i) * (1 - y_i))` +pub struct EqPoly; + +impl EqPoly { + /// This function builds `eq(x, y)` by fixing `y = r` and outputting the + /// evaluations over all `x` in `[0, 2^n)`. + pub fn fix_y_evals(r: &[F]) -> Vec { + // we build eq(x,r) from its evaluations + // we want to evaluate eq(x,r) over all binary strings `x` of length `n` + // for example, with n = 4, x is a binary string of length 4, then + // 0 0 0 0 -> (1-r0) * (1-r1) * (1-r2) * (1-r3) + // 1 0 0 0 -> r0 * (1-r1) * (1-r2) * (1-r3) + // 0 1 0 0 -> (1-r0) * r1 * (1-r2) * (1-r3) + // 1 1 0 0 -> r0 * r1 * (1-r2) * (1-r3) + // .... + // 1 1 1 1 -> r0 * r1 * r2 * r3 + // we will need 2^num_var evaluations + + // initializing the buffer with [1] + let mut buf = vec![F::one()]; + + for i in r.iter().rev() { + // suppose at the previous step we received [b_1, ..., b_k] + // for the current step we will need + // if x_i = 0: (1-ri) * [b_1, ..., b_k] + // if x_i = 1: ri * [b_1, ..., b_k] + buf = cfg_into_iter!(buf) + .flat_map(|j| { + let v = j * i; + [j - v, v] + }) + .collect(); + } + + buf + } + + /// Evaluate eq polynomial. + pub fn fix_xy_eval(x: &[F], y: &[F]) -> F { + debug_assert_eq!(x.len(), y.len()); + x.iter() + .zip(y.iter()) + .map(|(xi, yi)| xi.double() * yi - xi - yi + F::one()) + .product() } +} + +pub struct EqPolyVar; - buf +impl EqPolyVar { + /// Evaluate eq polynomial in circuit. + pub fn fix_xy_eval(x: &[FpVar], y: &[FpVar]) -> FpVar { + debug_assert_eq!(x.len(), y.len()); + let mut eval = FpVar::::one(); + for (xi, yi) in x.iter().zip(y.iter()) { + eval *= (xi + xi) * yi - xi - yi + F::one(); + } + eval + } } #[allow(clippy::filter_map_bool_then)] diff --git a/crates/primitives/src/traits.rs b/crates/primitives/src/traits.rs index 35d9be14e..9c97b277f 100644 --- a/crates/primitives/src/traits.rs +++ b/crates/primitives/src/traits.rs @@ -13,9 +13,15 @@ impl Dummy for Vec { } } -impl Dummy<()> for T { - fn dummy(_: ()) -> Self { - Default::default() +impl + Copy, const N: usize> Dummy for [T; N] { + fn dummy(cfg: Cfg) -> Self { + [T::dummy(cfg); N] + } +} + +impl, B: Dummy> Dummy for (A, B) { + fn dummy(cfg: Cfg) -> Self { + (A::dummy(cfg), B::dummy(cfg)) } } @@ -43,14 +49,14 @@ impl> Inputize for [T] { /// Note that we require this trait because we need to distinguish between some /// data types that are represented both natively and non-natively in-circuit /// (e.g., field elements can have type `FpVar` and `NonNativeUintVar`). -pub trait InputizeNonNative { - fn inputize_nonnative(&self) -> Vec; +pub trait InputizeEmulated { + fn inputize_emulated(&self) -> Vec; } -impl> InputizeNonNative for [T] { - fn inputize_nonnative(&self) -> Vec { +impl> InputizeEmulated for [T] { + fn inputize_emulated(&self) -> Vec { self.iter() - .flat_map(InputizeNonNative::::inputize_nonnative) + .flat_map(InputizeEmulated::::inputize_emulated) .collect() } } diff --git a/crates/primitives/src/transcripts/absorbable.rs b/crates/primitives/src/transcripts/absorbable.rs index 31c80dea7..8143a76d1 100644 --- a/crates/primitives/src/transcripts/absorbable.rs +++ b/crates/primitives/src/transcripts/absorbable.rs @@ -1,51 +1,53 @@ +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; use ark_relations::gr1cs::SynthesisError; -pub trait Absorbable { - fn absorb_into(&self, dest: &mut Vec); +pub trait Absorbable { + fn absorb_into(&self, dest: &mut Vec); - fn to_absorbable(&self) -> Vec { + fn to_absorbable(&self) -> Vec { let mut result = Vec::new(); self.absorb_into(&mut result); result } } -impl> Absorbable for usize { - fn absorb_into(&self, dest: &mut Vec) { +impl Absorbable for usize { + fn absorb_into(&self, dest: &mut Vec) { dest.push(F::from(*self as u64)); } } -impl> Absorbable for &T { - fn absorb_into(&self, dest: &mut Vec) { - >::absorb_into(self, dest); +impl Absorbable for &T { + fn absorb_into(&self, dest: &mut Vec) { + (*self).absorb_into(dest); } } -impl> Absorbable for (T, T) { - fn absorb_into(&self, dest: &mut Vec) { +impl Absorbable for (T, T) { + fn absorb_into(&self, dest: &mut Vec) { self.0.absorb_into(dest); self.1.absorb_into(dest); } } -impl> Absorbable for [T] { - fn absorb_into(&self, dest: &mut Vec) { +impl Absorbable for [T] { + fn absorb_into(&self, dest: &mut Vec) { for t in self.iter() { t.absorb_into(dest); } } } -impl, const N: usize> Absorbable for [T; N] { - fn absorb_into(&self, dest: &mut Vec) { - <[T] as Absorbable>::absorb_into(self, dest); +impl Absorbable for [T; N] { + fn absorb_into(&self, dest: &mut Vec) { + self.as_ref().absorb_into(dest); } } -impl> Absorbable for Vec { - fn absorb_into(&self, dest: &mut Vec) { - <[T] as Absorbable>::absorb_into(self, dest); +impl Absorbable for Vec { + fn absorb_into(&self, dest: &mut Vec) { + self.as_slice().absorb_into(dest); } } @@ -53,43 +55,43 @@ impl> Absorbable for Vec { /// is `F`. /// /// Matches `AbsorbGadget` in `ark-crypto-primitives`. -pub trait AbsorbableGadget { - fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError>; +pub trait AbsorbableGadget { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError>; - fn to_absorbable(&self) -> Result, SynthesisError> { + fn to_absorbable(&self) -> Result>, SynthesisError> { let mut result = Vec::new(); self.absorb_into(&mut result)?; Ok(result) } } -impl> AbsorbableGadget for &T { - fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { - >::absorb_into(self, dest) +impl> AbsorbableGadget for &T { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + (*self).absorb_into(dest) } } -impl> AbsorbableGadget for (T, T) { - fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { +impl> AbsorbableGadget for (T, T) { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.0.absorb_into(dest)?; self.1.absorb_into(dest) } } -impl> AbsorbableGadget for [T] { - fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { +impl> AbsorbableGadget for [T] { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.iter().try_for_each(|t| t.absorb_into(dest)) } } -impl, const N: usize> AbsorbableGadget for [T; N] { - fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { - <[T] as AbsorbableGadget>::absorb_into(self, dest) +impl, const N: usize> AbsorbableGadget for [T; N] { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.as_ref().absorb_into(dest) } } -impl> AbsorbableGadget for Vec { - fn absorb_into(&self, dest: &mut Vec) -> Result<(), SynthesisError> { - <[T] as AbsorbableGadget>::absorb_into(self, dest) +impl> AbsorbableGadget for Vec { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.as_slice().absorb_into(dest) } } diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs new file mode 100644 index 000000000..a16300dba --- /dev/null +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -0,0 +1,567 @@ +use ark_ff::{LegendreSymbol, PrimeField}; +use ark_r1cs_std::{ + alloc::AllocVar, + fields::{fp::FpVar, FieldVar}, + GR1CSVar, +}; +use ark_relations::gr1cs::SynthesisError; +use num_bigint::BigUint; +use sha3::{ + digest::{ExtendableOutput, Update, XofReader}, + Shake128, Shake128Reader, +}; + +pub mod sponge; + +pub fn field_element_from_shake(reader: &mut impl XofReader) -> F { + let mut buf = vec![0u8; F::MODULUS_BIT_SIZE.div_ceil(8) as usize]; + + loop { + reader.read(&mut buf); + if let Some(el) = F::from_random_bytes(&buf) { + return el; + } + } +} + +pub fn field_element_from_shake_without_0(reader: &mut impl XofReader) -> F { + loop { + let element = field_element_from_shake::(reader); + if !element.is_zero() { + return element; + } + } +} + +#[derive(Clone, Debug)] +pub struct GriffinParams { + pub(crate) round_constants: Vec>, + pub(crate) t: usize, + pub(crate) d: usize, + pub(crate) d_inv: Vec, + pub(crate) rounds: usize, + pub(crate) alpha_beta: Vec<[F; 2]>, + pub(crate) mat: Vec>, + pub rate: usize, + pub capacity: usize, +} + +impl GriffinParams { + pub const INIT_SHAKE: &'static str = "Griffin"; + + pub fn new(t: usize, d: usize, rounds: usize) -> Self { + assert!(t == 3 || t.is_multiple_of(4)); + assert!(d == 3 || d == 5); + assert!(rounds >= 1); + + let mut shake = Self::init_shake(); + + let d_inv = Self::calculate_d_inv(d as u64) + .to_radix_be(2) + .into_iter() + .map(|i| i != 0) + .skip_while(|i| !i) + .collect(); + let round_constants = Self::instantiate_rc(t, rounds, &mut shake); + let alpha_beta = Self::instantiate_alpha_beta(t, &mut shake); + + let mat = Self::instantiate_matrix(t); + + GriffinParams { + round_constants, + t, + d, + d_inv, + rounds, + alpha_beta, + mat, + rate: t - 1, + capacity: 1, + } + } + + fn calculate_d_inv(d: u64) -> BigUint { + let p_1 = -F::one(); + BigUint::from(d).modinv(&p_1.into()).unwrap() + } + + fn init_shake() -> Shake128Reader { + let mut shake = Shake128::default(); + shake.update(Self::INIT_SHAKE.as_bytes()); + for i in F::characteristic() { + shake.update(&i.to_le_bytes()); + } + shake.finalize_xof() + } + + fn instantiate_rc(t: usize, rounds: usize, shake: &mut Shake128Reader) -> Vec> { + (0..rounds - 1) + .map(|_| (0..t).map(|_| field_element_from_shake(shake)).collect()) + .collect() + } + + fn instantiate_alpha_beta(t: usize, shake: &mut Shake128Reader) -> Vec<[F; 2]> { + let mut alpha_beta = Vec::with_capacity(t - 2); + + // random alpha/beta + loop { + let alpha = field_element_from_shake_without_0::(shake); + let mut beta = field_element_from_shake_without_0::(shake); + // distinct + while alpha == beta { + beta = field_element_from_shake_without_0::(shake); + } + let mut symbol = alpha; + symbol.square_in_place(); + let mut tmp = beta; + tmp.double_in_place(); + tmp.double_in_place(); + symbol.sub_assign(&tmp); + if symbol.legendre() == LegendreSymbol::QuadraticNonResidue { + alpha_beta.push([alpha, beta]); + break; + } + } + + // other alphas/betas + for i in 2..t - 1 { + let mut alpha = alpha_beta[0][0]; + let mut beta = alpha_beta[0][1]; + alpha.mul_assign(&F::from(i as u64)); + beta.mul_assign(&F::from((i * i) as u64)); + // distinct + while alpha == beta { + beta = field_element_from_shake_without_0::(shake); + } + + #[cfg(debug_assertions)] + { + // check if really ok + let mut symbol = alpha; + symbol.square_in_place(); + let mut tmp = beta; + tmp.double_in_place(); + tmp.double_in_place(); + symbol.sub_assign(&tmp); + assert_eq!(symbol.legendre(), LegendreSymbol::QuadraticNonResidue); + } + + alpha_beta.push([alpha, beta]); + } + + alpha_beta + } + + fn circ_mat(row: &[F]) -> Vec> { + let t = row.len(); + let mut mat: Vec> = Vec::with_capacity(t); + let mut rot = row.to_owned(); + mat.push(rot.clone()); + for _ in 1..t { + rot.rotate_right(1); + mat.push(rot.clone()); + } + mat + } + + fn instantiate_matrix(t: usize) -> Vec> { + if t == 3 { + let row = vec![F::from(2), F::from(1), F::from(1)]; + Self::circ_mat(&row) + } else { + let row1 = vec![F::from(5), F::from(7), F::from(1), F::from(3)]; + let row2 = vec![F::from(4), F::from(6), F::from(1), F::from(1)]; + let row3 = vec![F::from(1), F::from(3), F::from(5), F::from(7)]; + let row4 = vec![F::from(1), F::from(1), F::from(4), F::from(6)]; + let c_mat = vec![row1, row2, row3, row4]; + if t == 4 { + c_mat + } else { + assert_eq!(t % 4, 0); + let mut mat: Vec> = vec![vec![F::zero(); t]; t]; + for (row, matrow) in mat.iter_mut().enumerate().take(t) { + for (col, matitem) in matrow.iter_mut().enumerate().take(t) { + let row_mod = row % 4; + let col_mod = col % 4; + *matitem = c_mat[row_mod][col_mod]; + if row / 4 == col / 4 { + matitem.add_assign(&c_mat[row_mod][col_mod]); + } + } + } + mat + } + } + } +} + +impl GriffinParams { + fn affine_3(&self, input: &mut [S], round: usize) { + // multiplication by circ(2 1 1) is equal to state + sum(state) + let mut sum = input[0]; + input.iter().skip(1).for_each(|el| sum.add_assign(el)); + + if round < self.rounds - 1 { + for (el, rc) in input.iter_mut().zip(self.round_constants[round].iter()) { + el.add_assign(&sum); + el.add_assign(rc); // add round constant + } + } else { + // no round constant + for el in input.iter_mut() { + el.add_assign(&sum); + } + } + } + + fn affine_4(&self, input: &mut [S], round: usize) { + let mut t_0 = input[0]; + t_0.add_assign(&input[1]); + let mut t_1 = input[2]; + t_1.add_assign(&input[3]); + let mut t_2 = input[1]; + t_2.double_in_place(); + t_2.add_assign(&t_1); + let mut t_3 = input[3]; + t_3.double_in_place(); + t_3.add_assign(&t_0); + let mut t_4 = t_1; + t_4.double_in_place(); + t_4.double_in_place(); + t_4.add_assign(&t_3); + let mut t_5 = t_0; + t_5.double_in_place(); + t_5.double_in_place(); + t_5.add_assign(&t_2); + let mut t_6 = t_3; + t_6.add_assign(&t_5); + let mut t_7 = t_2; + t_7.add_assign(&t_4); + input[0] = t_6; + input[1] = t_5; + input[2] = t_7; + input[3] = t_4; + + if round < self.rounds - 1 { + for (i, rc) in input.iter_mut().zip(self.round_constants[round].iter()) { + i.add_assign(rc); + } + } + } + + fn affine(&self, input: &mut [S], round: usize) { + if self.t == 3 { + self.affine_3(input, round); + return; + } + if self.t == 4 { + self.affine_4(input, round); + return; + } + + // first matrix + let t4 = self.t / 4; + for i in 0..t4 { + let start_index = i * 4; + let mut t_0 = input[start_index]; + t_0.add_assign(&input[start_index + 1]); + let mut t_1 = input[start_index + 2]; + t_1.add_assign(&input[start_index + 3]); + let mut t_2 = input[start_index + 1]; + t_2.double_in_place(); + t_2.add_assign(&t_1); + let mut t_3 = input[start_index + 3]; + t_3.double_in_place(); + t_3.add_assign(&t_0); + let mut t_4: S = t_1; + t_4.double_in_place(); + t_4.double_in_place(); + t_4.add_assign(&t_3); + let mut t_5 = t_0; + t_5.double_in_place(); + t_5.double_in_place(); + t_5.add_assign(&t_2); + input[start_index] = t_3 + t_5; + input[start_index + 1] = t_5; + input[start_index + 2] = t_2 + t_4; + input[start_index + 3] = t_4; + } + + // second matrix + let mut stored = [S::zero(); 4]; + for l in 0..4 { + stored[l] = input[l]; + for j in 1..t4 { + stored[l].add_assign(&input[4 * j + l]); + } + } + + for i in 0..input.len() { + input[i].add_assign(&stored[i % 4]); + if round < self.rounds - 1 { + input[i].add_assign(&self.round_constants[round][i]); // add round constant + } + } + } + + fn non_linear(&self, input: &mut [S]) { + // first two state words + input[0] = { + let mut res = S::one(); + for &i in &self.d_inv { + res.square_in_place(); + if i { + res *= input[0]; + } + } + res + }; + + let mut state = input[1]; + + input[1].square_in_place(); + match self.d { + 3 => {} + 5 => { + input[1].square_in_place(); + } + _ => panic!(), + } + input[1].mul_assign(&state); + + let mut y01_i = input[1]; + // rest of the state + for i in 2..input.len() { + y01_i += input[0]; + let l = if i == 2 { y01_i } else { y01_i + state }; + let ab = &self.alpha_beta[i - 2]; + state = input[i]; + input[i] *= l.square() + l * ab[0] + ab[1]; + } + } + + pub fn permute(&self, input: &mut [S]) { + self.affine(input, self.rounds); // no RC + + for r in 0..self.rounds { + self.non_linear(input); + self.affine(input, r); + } + } + + pub fn hash(&self, message: &[S]) -> S { + let mut state = vec![S::zero(); self.t]; + for chunk in message.chunks(self.rate) { + for i in 0..chunk.len() { + state[i] += &chunk[i]; + } + self.permute(&mut state) + } + state[0] + } +} + +impl GriffinParams { + fn non_linear_gadget(&self, state: &[FpVar]) -> Result>, SynthesisError> { + let cs = state.cs(); + let mut result = state.to_owned(); + // x0 + result[0] = FpVar::new_variable_with_inferred_mode(cs, || { + Ok({ + { + let v = result[0].value().unwrap_or_default(); + let mut res = F::one(); + for &i in &self.d_inv { + res.square_in_place(); + if i { + res *= v; + } + } + res + } + }) + })?; + + let mut sq = result[0].square()?; + if self.d == 5 { + sq = sq.square()?; + } + result[0].mul_equals(&sq, &state[0])?; + + // x1 + let mut sq = result[1].square()?; + if self.d == 5 { + sq = sq.square()?; + } + result[1] *= sq; + + let mut y01_i = result[1].clone(); + + // rest of the state + for i in 2..result.len() { + y01_i += &result[0]; + let l = if i == 2 { + y01_i.clone() + } else { + &y01_i + &state[i - 1] + }; + let ab = &self.alpha_beta[i - 2]; + result[i] *= l.square()? + l * ab[0] + ab[1]; + } + + Ok(result) + } + + pub fn permute_gadget(&self, state: &[FpVar]) -> Result>, SynthesisError> { + let mut current_state = state.to_owned(); + current_state = self + .mat + .iter() + .map(|row| current_state.iter().zip(row).map(|(a, b)| a * *b).sum()) + .collect(); + + for r in 0..self.rounds { + current_state = self.non_linear_gadget(¤t_state)?; + current_state = self + .mat + .iter() + .map(|row| current_state.iter().zip(row).map(|(a, b)| a * *b).sum()) + .collect(); + if r < self.rounds - 1 { + current_state = current_state + .iter() + .zip(&self.round_constants[r]) + .map(|(c, rc)| c + *rc) + .collect(); + } + } + Ok(current_state) + } + + pub fn hash_gadget(&self, message: &[FpVar]) -> Result, SynthesisError> { + let mut state = vec![FpVar::zero(); self.t]; + for chunk in message.chunks(self.rate) { + for i in 0..chunk.len() { + state[i] += &chunk[i]; + } + state = self.permute_gadget(&state)?; + } + Ok(state[0].clone()) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_ff::UniformRand; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::rand::thread_rng; + + use super::*; + + #[test] + fn test() { + let rng = &mut thread_rng(); + let griffin = GriffinParams::new(24, 5, 9); + let t = griffin.t; + let x: Vec = (0..t).map(|_| Fr::rand(rng)).collect(); + + let y = griffin.hash(&x); + + let cs = ConstraintSystem::new_ref(); + let x_var = Vec::new_witness(cs.clone(), || Ok(x.clone())).unwrap(); + let y_var = griffin.hash_gadget(&x_var).unwrap(); + assert_eq!(y, y_var.value().unwrap()); + println!("{}", cs.num_constraints()); + assert!(cs.is_satisfied().unwrap()); + } +} + +#[cfg(test)] +mod griffin_tests_bn256 { + use ark_bn254::Fr; + use ark_ff::UniformRand; + use ark_std::rand::thread_rng; + + use super::*; + + static TESTRUNS: usize = 5; + + #[test] + fn consistent_perm() { + let rng = &mut thread_rng(); + let griffin = GriffinParams::new(3, 5, 12); + let t = griffin.t; + for _ in 0..TESTRUNS { + let input1: Vec<_> = (0..t).map(|_| Fr::rand(rng)).collect(); + + let mut input2: Vec<_>; + loop { + input2 = (0..t).map(|_| Fr::rand(rng)).collect(); + if input1 != input2 { + break; + } + } + + let mut perm1 = input1.clone(); + let mut perm2 = input1.clone(); + let mut perm3 = input2.clone(); + griffin.permute(&mut perm1); + griffin.permute(&mut perm2); + griffin.permute(&mut perm3); + assert_eq!(perm1, perm2); + assert_ne!(perm1, perm3); + } + } + + fn matmul(input: &[F], mat: &[Vec]) -> Vec { + let t = mat.len(); + debug_assert!(t == input.len()); + let mut out = vec![F::zero(); t]; + for row in 0..t { + for (col, inp) in input.iter().enumerate() { + let mut tmp = mat[row][col]; + tmp *= inp; + out[row] += &tmp; + } + } + out + } + + fn affine_test(t: usize) { + let rng = &mut thread_rng(); + let griffin = GriffinParams::::new(t, 5, 1); + + let mat = &griffin.mat; + + for _ in 0..TESTRUNS { + let input: Vec = (0..t).map(|_| F::rand(rng)).collect(); + + // affine 1 + let output1 = matmul(&input, mat); + let mut output2 = input.to_owned(); + griffin.affine(&mut output2, 1); + assert_eq!(output1, output2); + } + } + + #[test] + fn affine_3() { + affine_test::(3); + } + + #[test] + fn affine_4() { + affine_test::(4); + } + + #[test] + fn affine_8() { + affine_test::(8); + } + + #[test] + fn affine_60() { + affine_test::(60); + } +} diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs new file mode 100644 index 000000000..314486d68 --- /dev/null +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -0,0 +1,471 @@ +use ark_crypto_primitives::sponge::DuplexSpongeMode; +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::{ + fields::{fp::FpVar, FieldVar}, + prelude::{Boolean, ToBitsGadget}, +}; +use ark_relations::gr1cs::SynthesisError; +use ark_std::sync::Arc; + +use crate::transcripts::{griffin::GriffinParams, AbsorbableGadget, Transcript, TranscriptVar}; + +#[derive(Clone)] +pub struct GriffinSponge { + /// Sponge Config + pub griffin: Arc>, + + // Sponge State + /// Current sponge's state (current elements in the permutation block) + pub state: Vec, + /// Current mode (whether its absorbing or squeezing) + pub mode: DuplexSpongeMode, +} + +impl GriffinSponge { + fn permute(&mut self) { + self.griffin.permute(&mut self.state); + } + + // Absorbs everything in elements, this does not end in an absorbtion. + fn absorb_internal(&mut self, mut rate_start_index: usize, elements: &[F]) { + let mut remaining_elements = elements; + + loop { + // if we can finish in this call + if rate_start_index + remaining_elements.len() <= self.griffin.rate { + for (i, element) in remaining_elements.iter().enumerate() { + self.state[self.griffin.capacity + i + rate_start_index] += element; + } + self.mode = DuplexSpongeMode::Absorbing { + next_absorb_index: rate_start_index + remaining_elements.len(), + }; + + return; + } + // otherwise absorb (rate - rate_start_index) elements + let num_elements_absorbed = self.griffin.rate - rate_start_index; + for (i, element) in remaining_elements + .iter() + .enumerate() + .take(num_elements_absorbed) + { + self.state[self.griffin.capacity + i + rate_start_index] += element; + } + self.permute(); + // the input elements got truncated by num elements absorbed + remaining_elements = &remaining_elements[num_elements_absorbed..]; + rate_start_index = 0; + } + } + + // Squeeze |output| many elements. This does not end in a squeeze + fn squeeze_internal(&mut self, mut rate_start_index: usize, output: &mut [F]) { + let mut output_remaining = output; + loop { + // if we can finish in this call + if rate_start_index + output_remaining.len() <= self.griffin.rate { + output_remaining.clone_from_slice( + &self.state[self.griffin.capacity + rate_start_index + ..(self.griffin.capacity + output_remaining.len() + rate_start_index)], + ); + self.mode = DuplexSpongeMode::Squeezing { + next_squeeze_index: rate_start_index + output_remaining.len(), + }; + return; + } + // otherwise squeeze (rate - rate_start_index) elements + let num_elements_squeezed = self.griffin.rate - rate_start_index; + output_remaining[..num_elements_squeezed].clone_from_slice( + &self.state[self.griffin.capacity + rate_start_index + ..(self.griffin.capacity + num_elements_squeezed + rate_start_index)], + ); + + // Repeat with updated output slices + output_remaining = &mut output_remaining[num_elements_squeezed..]; + // Unless we are done with squeezing in this call, permute. + if !output_remaining.is_empty() { + self.permute(); + } + + rate_start_index = 0; + } + } +} + +#[derive(Clone)] +pub struct GriffinSpongeVar { + /// Sponge Parameters + pub griffin: Arc>, + + // Sponge State + /// The sponge's state + pub state: Vec>, + /// The mode + pub mode: DuplexSpongeMode, +} + +impl GriffinSpongeVar { + fn permute(&mut self) -> Result<(), SynthesisError> { + self.state = self.griffin.permute_gadget(&self.state)?; + Ok(()) + } + + fn absorb_internal( + &mut self, + mut rate_start_index: usize, + elements: &[FpVar], + ) -> Result<(), SynthesisError> { + let mut remaining_elements = elements; + loop { + // if we can finish in this call + if rate_start_index + remaining_elements.len() <= self.griffin.rate { + for (i, element) in remaining_elements.iter().enumerate() { + self.state[self.griffin.capacity + i + rate_start_index] += element; + } + self.mode = DuplexSpongeMode::Absorbing { + next_absorb_index: rate_start_index + remaining_elements.len(), + }; + + return Ok(()); + } + // otherwise absorb (rate - rate_start_index) elements + let num_elements_absorbed = self.griffin.rate - rate_start_index; + for (i, element) in remaining_elements + .iter() + .enumerate() + .take(num_elements_absorbed) + { + self.state[self.griffin.capacity + i + rate_start_index] += element; + } + self.permute()?; + // the input elements got truncated by num elements absorbed + remaining_elements = &remaining_elements[num_elements_absorbed..]; + rate_start_index = 0; + } + } + + // Squeeze |output| many elements. This does not end in a squeeze + fn squeeze_internal( + &mut self, + mut rate_start_index: usize, + output: &mut [FpVar], + ) -> Result<(), SynthesisError> { + let mut remaining_output = output; + loop { + // if we can finish in this call + if rate_start_index + remaining_output.len() <= self.griffin.rate { + remaining_output.clone_from_slice( + &self.state[self.griffin.capacity + rate_start_index + ..(self.griffin.capacity + remaining_output.len() + rate_start_index)], + ); + self.mode = DuplexSpongeMode::Squeezing { + next_squeeze_index: rate_start_index + remaining_output.len(), + }; + return Ok(()); + } + // otherwise squeeze (rate - rate_start_index) elements + let num_elements_squeezed = self.griffin.rate - rate_start_index; + remaining_output[..num_elements_squeezed].clone_from_slice( + &self.state[self.griffin.capacity + rate_start_index + ..(self.griffin.capacity + num_elements_squeezed + rate_start_index)], + ); + + // Repeat with updated output slices and rate start index + remaining_output = &mut remaining_output[num_elements_squeezed..]; + + // Unless we are done with squeezing in this call, permute. + if !remaining_output.is_empty() { + self.permute()?; + } + rate_start_index = 0; + } + } +} + +impl Transcript for GriffinSponge { + type Config = Arc>; + + fn new(parameters: &Arc>) -> Self { + let state = vec![F::zero(); parameters.rate + parameters.capacity]; + let mode = DuplexSpongeMode::Absorbing { + next_absorb_index: 0, + }; + + Self { + griffin: parameters.clone(), + state, + mode, + } + } + + fn add_field_elements(&mut self, elems: &[F]) -> &mut Self { + if elems.is_empty() { + return self; + } + + match self.mode { + DuplexSpongeMode::Absorbing { next_absorb_index } => { + let mut absorb_index = next_absorb_index; + if absorb_index == self.griffin.rate { + self.permute(); + absorb_index = 0; + } + self.absorb_internal(absorb_index, elems); + } + DuplexSpongeMode::Squeezing { + next_squeeze_index: _, + } => { + self.absorb_internal(0, elems); + } + }; + self + } + + fn get_bits(&mut self, num_bits: usize) -> Vec { + let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize; + + let num_elements = num_bits.div_ceil(usable_bits); + let src_elements = self.get_field_elements(num_elements); + + let mut bits: Vec = Vec::with_capacity(usable_bits * num_elements); + for elem in &src_elements { + let elem_bits = elem.into_bigint().to_bits_le(); + bits.extend_from_slice(&elem_bits[..usable_bits]); + } + + bits.truncate(num_bits); + bits + } + + fn get_field_elements(&mut self, num_elements: usize) -> Vec { + let mut squeezed_elems = vec![F::zero(); num_elements]; + match self.mode { + DuplexSpongeMode::Absorbing { + next_absorb_index: _, + } => { + self.permute(); + self.squeeze_internal(0, &mut squeezed_elems); + } + DuplexSpongeMode::Squeezing { next_squeeze_index } => { + let mut squeeze_index = next_squeeze_index; + if squeeze_index == self.griffin.rate { + self.permute(); + squeeze_index = 0; + } + self.squeeze_internal(squeeze_index, &mut squeezed_elems); + } + }; + + squeezed_elems + } +} + +impl TranscriptVar for GriffinSpongeVar { + type Native = GriffinSponge; + + fn new(parameters: &Arc>) -> Self + where + Self: Sized, + { + let zero = FpVar::::zero(); + let state = vec![zero; parameters.rate + parameters.capacity]; + let mode = DuplexSpongeMode::Absorbing { + next_absorb_index: 0, + }; + + Self { + griffin: parameters.clone(), + state, + mode, + } + } + + fn add + ?Sized>( + &mut self, + input: &A, + ) -> Result<&mut Self, SynthesisError> { + let input = input.to_absorbable()?; + if input.is_empty() { + return Ok(self); + } + + match self.mode { + DuplexSpongeMode::Absorbing { next_absorb_index } => { + let mut absorb_index = next_absorb_index; + if absorb_index == self.griffin.rate { + self.permute()?; + absorb_index = 0; + } + self.absorb_internal(absorb_index, input.as_slice())?; + } + DuplexSpongeMode::Squeezing { + next_squeeze_index: _, + } => { + self.absorb_internal(0, input.as_slice())?; + } + }; + + Ok(self) + } + + fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError> { + let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize; + + let num_elements = num_bits.div_ceil(usable_bits); + let src_elements = self.get_field_elements(num_elements)?; + + let mut bits: Vec> = Vec::with_capacity(usable_bits * num_elements); + for elem in &src_elements { + bits.extend_from_slice(&elem.to_bits_le()?[..usable_bits]); + } + + bits.truncate(num_bits); + Ok(bits) + } + + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError> { + let zero = FpVar::zero(); + let mut squeezed_elems = vec![zero; num_elements]; + match self.mode { + DuplexSpongeMode::Absorbing { + next_absorb_index: _, + } => { + self.permute()?; + self.squeeze_internal(0, &mut squeezed_elems)?; + } + DuplexSpongeMode::Squeezing { next_squeeze_index } => { + let mut squeeze_index = next_squeeze_index; + if squeeze_index == self.griffin.rate { + self.permute()?; + squeeze_index = 0; + } + self.squeeze_internal(squeeze_index, &mut squeezed_elems)?; + } + }; + + Ok(squeezed_elems) + } +} + +#[cfg(test)] +pub mod tests { + use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; + use ark_ec::PrimeGroup; + use ark_ff::{BigInteger, PrimeField, UniformRand}; + use ark_r1cs_std::{ + alloc::AllocVar, + fields::fp::FpVar, + groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, + GR1CSVar, + }; + use ark_relations::gr1cs::ConstraintSystem; + use ark_std::{error::Error, test_rng}; + + use super::*; + use crate::algebra::group::emulated::EmulatedAffineVar; + + #[test] + fn test_transcript_and_transcriptvar_absorb_native_point() -> Result<(), Box> { + // use 'native' transcript + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(&config); + let rng = &mut test_rng(); + + let p = G1::rand(rng); + tr.add(&p); + let c = tr.challenge_field_element(); + + // use 'gadget' transcript + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(&config); + let p_var = ProjectiveVar::>::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; + + // assert that native & gadget transcripts return the same challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_transcript_and_transcriptvar_absorb_nonnative_point() -> Result<(), Box> { + // use 'native' transcript + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(&config); + let rng = &mut test_rng(); + + let p = G1::rand(rng); + tr.add(&p); + let c = tr.challenge_field_element(); + + // use 'gadget' transcript + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(&config); + let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; + let c_var = tr_var.challenge_field_element()?; + + // assert that native & gadget transcripts return the same challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_transcript_and_transcriptvar_get_challenge() -> Result<(), Box> { + // use 'native' transcript + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(&config); + tr.add(&Fr::from(42_u32)); + let c = tr.challenge_field_element(); + + // use 'gadget' transcript + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(&config); + let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_field_element()?; + + // assert that native & gadget transcripts return the same challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_transcript_and_transcriptvar_nbits() -> Result<(), Box> { + let nbits = 128; + + // use 'native' transcript + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(&config); + tr.add(&Fq::from(42_u32)); + + // get challenge from native transcript + let c_bits = tr.challenge_bits(nbits); + + // use 'gadget' transcript + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(&config); + let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; + tr_var.add(&v)?; + + // get challenge from circuit transcript + let c_var = tr_var.challenge_bits(nbits)?; + + let p = G1::generator(); + let p_var = GVar::new_witness(cs.clone(), || Ok(p))?; + + // multiply point P by the challenge in different formats, to ensure that we get the same + // result natively and in-circuit + let c = Fr::from(::BigInt::from_bits_le(&c_bits)); + + // check that native c*P and in-circuit c*P using scalar_mul_le are equal + assert_eq!(p * c, p_var.scalar_mul_le(c_var.iter())?.value()?); + // check that native c*P using mul_bits_be and in-circuit c*P using scalar_mul_le are equal + // (notice the .rev to convert the LE to BE) + assert_eq!( + p.mul_bits_be(c_bits.into_iter().rev()), + p_var.scalar_mul_le(c_var.iter())?.value()? + ); + Ok(()) + } +} diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index 21c709f19..4b6820a04 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -1,41 +1,49 @@ -use ark_crypto_primitives::sponge::{ - constraints::CryptographicSpongeVar, CryptographicSponge, FieldElementSize, -}; -use ark_ec::CurveGroup; -use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; -use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; - pub use absorbable::{Absorbable, AbsorbableGadget}; +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; +use ark_relations::gr1cs::SynthesisError; pub mod absorbable; +pub mod griffin; pub mod poseidon; pub trait Transcript { + type Config; + + fn new(config: &Self::Config) -> Self; + /// `new_with_pp_hash` creates a new transcript / sponge with the given /// hash of the public parameters. fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self where - F: Absorbable, - Self: CryptographicSponge, + Self: Sized, { let mut sponge = Self::new(config); - sponge.add(&pp_hash); + sponge.add_field_elements(&[pp_hash]); sponge } - fn add + ?Sized>(&mut self, input: &A); + fn add(&mut self, input: &A) -> &mut Self { + let elems = input.to_absorbable(); + + self.add_field_elements(&elems) + } + + fn add_field_elements(&mut self, input: &[F]) -> &mut Self; /// Squeeze `num_bits` bits from the sponge. fn get_bits(&mut self, num_bits: usize) -> Vec; + fn get_field_element(&mut self) -> F { + self.get_field_elements(1)[0] + } + fn get_field_elements(&mut self, num_elements: usize) -> Vec; /// Creates a new sponge with applied domain separation. fn separate_domain(&self, domain: &[u8]) -> Self where - F: Absorbable, - Self: CryptographicSponge, + Self: Clone, { let mut new_sponge = self.clone(); @@ -47,66 +55,75 @@ pub trait Transcript { .map(|chunk| F::from_le_bytes_mod_order(chunk)) .collect::>(); - new_sponge.add(&limbs); + new_sponge.add_field_elements(&limbs); new_sponge } - fn challenge_field_element(&mut self) -> F - where - F: Absorbable, - { + fn challenge_field_element(&mut self) -> F { let c = self.get_field_elements(1); - self.add(&c[0]); + self.add_field_elements(&c); c[0] } - fn challenge_bits(&mut self, nbits: usize) -> Vec - where - F: Absorbable, - { + + fn challenge_bits(&mut self, nbits: usize) -> Vec { let bits = self.get_bits(nbits); - self.add(&F::from(F::BigInt::from_bits_le(&bits))); + self.add_field_elements( + &bits + .chunks(F::MODULUS_BIT_SIZE as usize - 1) + .map(F::BigInt::from_bits_le) + .map(F::from) + .collect::>(), + ); bits } - fn challenge_field_elements(&mut self, n: usize) -> Vec - where - F: Absorbable, - { + + fn challenge_field_elements(&mut self, n: usize) -> Vec { let c = self.get_field_elements(n); - self.add(&c); + self.add_field_elements(&c); c } } pub trait TranscriptVar { - type Native; + type Native: Transcript; + + fn new(config: &>::Config) -> Self + where + Self: Sized; /// `new_with_pp_hash` creates a new transcript / sponge with the given /// hash of the public parameters. fn new_with_pp_hash( - config: &Self::Parameters, + config: &>::Config, pp_hash: &FpVar, ) -> Result where - Self: CryptographicSpongeVar, - Self::Native: CryptographicSponge, + Self: Sized, { - let mut sponge = Self::new(ConstraintSystemRef::None, config); + let mut sponge = Self::new(config); sponge.add(&pp_hash)?; Ok(sponge) } - fn add>>(&mut self, input: &A) -> Result<(), SynthesisError>; + fn add + ?Sized>( + &mut self, + input: &A, + ) -> Result<&mut Self, SynthesisError>; /// Squeeze `num_bits` bits from the sponge. fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError>; + fn get_field_element(&mut self) -> Result, SynthesisError> { + Ok(self.get_field_elements(1)?.pop().unwrap()) + } + fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError>; /// Creates a new sponge with applied domain separation. fn separate_domain(&self, domain: &[u8]) -> Result where - Self: CryptographicSponge, + Self: Clone, { let mut new_sponge = self.clone(); @@ -128,9 +145,15 @@ pub trait TranscriptVar { self.add(&c[0])?; Ok(c.pop().unwrap()) } + fn challenge_bits(&mut self, nbits: usize) -> Result>, SynthesisError> { let bits = self.get_bits(nbits)?; - self.add(&Boolean::le_bits_to_fp(&bits)?)?; + self.add( + &bits + .chunks(F::MODULUS_BIT_SIZE as usize - 1) + .map(Boolean::le_bits_to_fp) + .collect::, _>>()?, + )?; Ok(bits) } diff --git a/crates/primitives/src/transcripts/poseidon/mod.rs b/crates/primitives/src/transcripts/poseidon/mod.rs new file mode 100644 index 000000000..5693fb77c --- /dev/null +++ b/crates/primitives/src/transcripts/poseidon/mod.rs @@ -0,0 +1,37 @@ +use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, find_poseidon_ark_and_mds}; +use ark_ff::PrimeField; + +pub mod sponge; + +/// This Poseidon configuration generator produces a Poseidon configuration with custom parameters +pub fn poseidon_custom_config( + full_rounds: usize, + partial_rounds: usize, + alpha: u64, + rate: usize, + capacity: usize, +) -> PoseidonConfig { + let (ark, mds) = find_poseidon_ark_and_mds::( + F::MODULUS_BIT_SIZE as u64, + rate, + full_rounds as u64, + partial_rounds as u64, + 0, + ); + + PoseidonConfig::new(full_rounds, partial_rounds, alpha, mds, ark, rate, capacity) +} + +/// This Poseidon configuration generator agrees with Circom's Poseidon(4) in the case of BN254's scalar field +pub fn poseidon_canonical_config() -> PoseidonConfig { + // 120 bit security target as in + // https://eprint.iacr.org/2019/458.pdf + // t = rate + 1 + + let full_rounds = 8; + let partial_rounds = 60; + let alpha = 5; + let rate = 4; + + poseidon_custom_config(full_rounds, partial_rounds, alpha, rate, 1) +} diff --git a/crates/primitives/src/transcripts/poseidon.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs similarity index 71% rename from crates/primitives/src/transcripts/poseidon.rs rename to crates/primitives/src/transcripts/poseidon/sponge.rs index 1b10ef38a..9601d8bbe 100644 --- a/crates/primitives/src/transcripts/poseidon.rs +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -1,24 +1,25 @@ use ark_crypto_primitives::sponge::{ constraints::CryptographicSpongeVar, - poseidon::{ - constraints::PoseidonSpongeVar, find_poseidon_ark_and_mds, PoseidonConfig, PoseidonSponge, - }, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, Absorb, CryptographicSponge, FieldBasedCryptographicSponge, }; -use ark_ec::CurveGroup; use ark_ff::PrimeField; -use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar, groups::CurveVar}; -use ark_relations::gr1cs::SynthesisError; +use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; +use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; use ark_std::mem::transmute_copy; -use crate::transcripts::Absorbable; - -use super::{AbsorbableGadget, Transcript, TranscriptVar}; +use crate::transcripts::{AbsorbableGadget, Transcript, TranscriptVar}; impl Transcript for PoseidonSponge { - fn add + ?Sized>(&mut self, input: &A) { + type Config = PoseidonConfig; + + fn new(config: &Self::Config) -> Self { + CryptographicSponge::new(config) + } + + fn add_field_elements(&mut self, input: &[F]) -> &mut Self { struct Hack(I); - impl Absorb for Hack> { + impl Absorb for Hack<&[F]> { fn to_sponge_bytes(&self, _: &mut Vec) { // Unreachable because `PoseidonSponge::absorb` only calls // `to_sponge_field_elements_as_vec::` @@ -29,11 +30,11 @@ impl Transcript for PoseidonSponge { // Safe because `F` in `to_sponge_field_elements_as_vec::`, // which is called by `PoseidonSponge::absorb`, is the same as // `T` here. - dest.extend(unsafe { transmute_copy::<&[F], &[T]>(&self.0.as_ref()) }); + dest.extend(unsafe { transmute_copy::<&[F], &[T]>(&self.0) }); } } - let v = input.to_absorbable(); - CryptographicSponge::absorb(self, &Hack(v)); + CryptographicSponge::absorb(self, &Hack(input)); + self } fn get_bits(&mut self, num_bits: usize) -> Vec { @@ -48,8 +49,19 @@ impl Transcript for PoseidonSponge { impl TranscriptVar for PoseidonSpongeVar { type Native = PoseidonSponge; - fn add>>(&mut self, input: &A) -> Result<(), SynthesisError> { - self.absorb(&input.to_absorbable()?) + fn new(config: &PoseidonConfig) -> Self + where + Self: Sized, + { + CryptographicSpongeVar::new(ConstraintSystemRef::None, config) + } + + fn add + ?Sized>( + &mut self, + input: &A, + ) -> Result<&mut Self, SynthesisError> { + self.absorb(&input.to_absorbable()?)?; + Ok(self) } fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError> { @@ -61,59 +73,31 @@ impl TranscriptVar for PoseidonSpongeVar { } } -/// This Poseidon configuration generator produces a Poseidon configuration with custom parameters -pub fn poseidon_custom_config( - full_rounds: usize, - partial_rounds: usize, - alpha: u64, - rate: usize, - capacity: usize, -) -> PoseidonConfig { - let (ark, mds) = find_poseidon_ark_and_mds::( - F::MODULUS_BIT_SIZE as u64, - rate, - full_rounds as u64, - partial_rounds as u64, - 0, - ); - - PoseidonConfig::new(full_rounds, partial_rounds, alpha, mds, ark, rate, capacity) -} - -/// This Poseidon configuration generator agrees with Circom's Poseidon(4) in the case of BN254's scalar field -pub fn poseidon_canonical_config() -> PoseidonConfig { - // 120 bit security target as in - // https://eprint.iacr.org/2019/458.pdf - // t = rate + 1 - - let full_rounds = 8; - let partial_rounds = 60; - let alpha = 5; - let rate = 4; - - poseidon_custom_config(full_rounds, partial_rounds, alpha, rate, 1) -} - #[cfg(test)] pub mod tests { use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; + use ark_crypto_primitives::sponge::poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}; use ark_ec::PrimeGroup; - use ark_ff::{BigInteger, UniformRand}; + use ark_ff::{BigInteger, PrimeField, UniformRand}; use ark_r1cs_std::{ - alloc::AllocVar, groups::curves::short_weierstrass::ProjectiveVar, GR1CSVar, + alloc::AllocVar, + fields::fp::FpVar, + groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, + GR1CSVar, }; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, str::FromStr, test_rng}; - use crate::algebra::group::nonnative::NonNativeAffineVar; - - use super::*; + use crate::{ + algebra::group::emulated::EmulatedAffineVar, + transcripts::{poseidon::poseidon_canonical_config, Transcript, TranscriptVar}, + }; // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 #[test] fn check_against_circom_poseidon() -> Result<(), Box> { let config = poseidon_canonical_config::(); - let mut poseidon_sponge: PoseidonSponge<_> = CryptographicSponge::new(&config); + let mut poseidon_sponge = PoseidonSponge::new(&config); let v = vec![1, 2, 3, 4] .into_iter() .map(Fr::from) @@ -143,11 +127,8 @@ pub mod tests { // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); - let p_var = ProjectiveVar::>::new_witness( - ConstraintSystem::::new_ref(), - || Ok(p), - )?; + let mut tr_var = PoseidonSpongeVar::::new(&config); + let p_var = ProjectiveVar::>::new_witness(cs, || Ok(p))?; tr_var.add(&p_var)?; let c_var = tr_var.challenge_field_element()?; @@ -169,9 +150,8 @@ pub mod tests { // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); - let p_var = - NonNativeAffineVar::::new_witness(ConstraintSystem::::new_ref(), || Ok(p))?; + let mut tr_var = PoseidonSpongeVar::::new(&config); + let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; tr_var.add(&p_var)?; let c_var = tr_var.challenge_field_element()?; @@ -190,7 +170,7 @@ pub mod tests { // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); + let mut tr_var = PoseidonSpongeVar::::new(&config); let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; tr_var.add(&v)?; let c_var = tr_var.challenge_field_element()?; @@ -214,7 +194,7 @@ pub mod tests { // use 'gadget' transcript let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(cs.clone(), &config); + let mut tr_var = PoseidonSpongeVar::::new(&config); let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; tr_var.add(&v)?; diff --git a/crates/primitives/src/utils/mod.rs b/crates/primitives/src/utils/mod.rs new file mode 100644 index 000000000..1e8ac48a7 --- /dev/null +++ b/crates/primitives/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod null; +// pub mod vec; diff --git a/crates/primitives/src/utils/null.rs b/crates/primitives/src/utils/null.rs new file mode 100644 index 000000000..6d043534b --- /dev/null +++ b/crates/primitives/src/utils/null.rs @@ -0,0 +1,75 @@ +use ark_ff::Field; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + GR1CSVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + fmt::Debug, + iter::Sum, + ops::{Add, Mul}, +}; + +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub struct Null; + +impl Add for Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Add for &Null { + type Output = Null; + + fn add(self, _: F) -> Null { + Null + } +} + +impl Mul for Null { + type Output = Self; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Mul for &Null { + type Output = Null; + + fn mul(self, _: F) -> Null { + Null + } +} + +impl Sum for Null { + fn sum>(_: I) -> Self { + Null + } +} + +impl AllocVar for Null { + fn new_variable>( + _cs: impl Into>, + _f: impl FnOnce() -> Result, + _mode: AllocationMode, + ) -> Result { + Ok(Self) + } +} + +impl GR1CSVar for Null { + type Value = Null; + + fn cs(&self) -> ConstraintSystemRef { + ConstraintSystemRef::None + } + + fn value(&self) -> Result { + Ok(Null) + } +} diff --git a/crates/primitives/src/utils/vec.rs b/crates/primitives/src/utils/vec.rs new file mode 100644 index 000000000..f1c9fec21 --- /dev/null +++ b/crates/primitives/src/utils/vec.rs @@ -0,0 +1,109 @@ +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, + GR1CSVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + fmt::Debug, + ops::{Deref, DerefMut}, +}; + +use crate::{ + arithmetizations::ArithConfig, + circuits::var::Var, + traits::Dummy, + transcripts::{Absorbable, AbsorbableGadget}, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WrappedVec(Vec); + +impl Deref for WrappedVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WrappedVec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for WrappedVec { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl Absorbable for WrappedVec { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest) + } +} + +impl> AbsorbableGadget for WrappedVec { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.0.absorb_into(dest) + } +} + +impl, Y, F: Field> AllocVar, F> for WrappedVec { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let v = f()?; + Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(|v| Self(v)) + } +} + +impl> CondSelectGadget for WrappedVec { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.len() != false_value.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(WrappedVec( + true_value + .0 + .iter() + .zip(false_value.0.iter()) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + )) + } +} + +impl> GR1CSVar for WrappedVec { + type Value = WrappedVec; + + fn cs(&self) -> ConstraintSystemRef { + self.0.cs() + } + + fn value(&self) -> Result { + self.0.value().map(WrappedVec) + } +} + +impl> Var for WrappedVec { + type Native = WrappedVec; +} + +impl Dummy<&A> for WrappedVec { + fn dummy(cfg: &A) -> Self { + vec![V::default(); cfg.n_public_inputs()].into() + } +} From 638f248569dd01dc9f3e66c62a405d92b5bf4416 Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 18 Nov 2025 03:02:00 +0800 Subject: [PATCH 08/93] More polynomial and power utils --- crates/primitives/src/algebra/ops/mod.rs | 1 + crates/primitives/src/algebra/ops/poly.rs | 44 ++++++++++++++++++ crates/primitives/src/algebra/ops/pow.rs | 54 +++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 crates/primitives/src/algebra/ops/poly.rs diff --git a/crates/primitives/src/algebra/ops/mod.rs b/crates/primitives/src/algebra/ops/mod.rs index a0f820298..bbd850498 100644 --- a/crates/primitives/src/algebra/ops/mod.rs +++ b/crates/primitives/src/algebra/ops/mod.rs @@ -1,6 +1,7 @@ pub mod bits; pub mod eq; pub mod matrix; +pub mod poly; pub mod pow; pub mod rlc; pub mod vector; diff --git a/crates/primitives/src/algebra/ops/poly.rs b/crates/primitives/src/algebra/ops/poly.rs new file mode 100644 index 000000000..53b5c345a --- /dev/null +++ b/crates/primitives/src/algebra/ops/poly.rs @@ -0,0 +1,44 @@ +use ark_ff::PrimeField; +use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; +use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use ark_relations::gr1cs::SynthesisError; + +use super::pow::Pow; + +pub trait EvaluationDomainGadget { + fn evaluate_all_lagrange_coefficients_var( + &self, + tau: &FpVar, + ) -> Result>, SynthesisError>; + + fn evaluate_vanishing_polynomial_var(&self, tau: &FpVar) + -> Result, SynthesisError>; +} + +impl EvaluationDomainGadget for GeneralEvaluationDomain { + fn evaluate_all_lagrange_coefficients_var( + &self, + tau: &FpVar, + ) -> Result>, SynthesisError> { + let size = self.size() as u64; + let size_inv = self.size_inv(); + let offset = self.coset_offset(); + let offset_inv = self.coset_offset_inv(); + let group_gen = self.group_gen(); + + let l_i = (tau.pow_by_constant([size])? * offset_inv.pow([size - 1]) - offset) * size_inv; + + group_gen + .powers(size as usize) + .into_iter() + .map(|g| (&l_i * g).mul_by_inverse(&(tau - offset * g))) + .collect() + } + + fn evaluate_vanishing_polynomial_var( + &self, + tau: &FpVar, + ) -> Result, SynthesisError> { + Ok(tau.pow_by_constant([self.size() as u64])? - self.coset_offset_pow_size()) + } +} diff --git a/crates/primitives/src/algebra/ops/pow.rs b/crates/primitives/src/algebra/ops/pow.rs index 0406ba855..918577115 100644 --- a/crates/primitives/src/algebra/ops/pow.rs +++ b/crates/primitives/src/algebra/ops/pow.rs @@ -2,7 +2,14 @@ use ark_ff::{Field, PrimeField}; use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; pub trait Pow: Sized { + /// Compute `self^0, self^1, ..., self^{n-1}` fn powers(&self, n: usize) -> Vec; + + /// Compute `self^{2^0}, self^{2^1}, ..., self^{2^{n-1}}` + fn repeated_squares(&self, n: usize) -> Vec; + + /// Compute `self^0, self^1, ..., self^{2^n - 1}` from repeated squares + fn powers_from_repeated_squares(squares: &[Self]) -> Vec; } impl Pow for F { @@ -13,10 +20,34 @@ impl Pow for F { } res } + + fn repeated_squares(&self, n: usize) -> Vec { + if n == 0 { + return vec![]; + } + let mut res = vec![F::zero(); n]; + res[0] = *self; + for i in 1..n { + res[i] = res[i - 1].square(); + } + res + } + + fn powers_from_repeated_squares(squares: &[Self]) -> Vec { + let mut pows = vec![F::one()]; + for square in squares.iter().rev() { + pows = pows.into_iter().flat_map(|e| [e, e * square]).collect(); + } + pows + } } pub trait PowGadget: Sized { fn powers(&self, n: usize) -> Vec; + + fn repeated_squares(&self, n: usize) -> Vec; + + fn powers_from_repeated_squares(squares: &[Self]) -> Vec; } impl PowGadget for FpVar { @@ -27,4 +58,27 @@ impl PowGadget for FpVar { } res } + + fn repeated_squares(&self, n: usize) -> Vec { + if n == 0 { + return vec![]; + } + let mut res = vec![FpVar::zero(); n]; + res[0] = self.clone(); + for i in 1..n { + res[i] = &res[i - 1] * &res[i - 1]; + } + res + } + + fn powers_from_repeated_squares(squares: &[Self]) -> Vec { + let mut pows = vec![FpVar::one()]; + for square in squares.iter().rev() { + pows = pows + .into_iter() + .flat_map(|e| [e.clone(), e * square]) + .collect(); + } + pows + } } From 44d66285f9e147bf11cb7239bd77cb8eec3b0363 Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 18 Nov 2025 03:04:37 +0800 Subject: [PATCH 09/93] Prefer ark_std over std --- crates/primitives/src/arithmetizations/ccs/mod.rs | 4 +--- crates/primitives/src/arithmetizations/mod.rs | 3 +-- crates/primitives/src/circuits/mod.rs | 3 +-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index dee3af2de..6bed62774 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -1,9 +1,7 @@ -use std::fmt::Debug; - use ark_ff::Field; use ark_poly::DenseMultilinearExtension; use ark_relations::gr1cs::{ConstraintSystem, Matrix}; -use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, log2, marker::PhantomData}; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, fmt::Debug, log2, marker::PhantomData}; #[cfg(feature = "parallel")] use rayon::prelude::*; diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index 28073b34d..0866a7d11 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,6 +1,5 @@ -use std::fmt::Debug; - use ark_relations::gr1cs::SynthesisError; +use ark_std::fmt::Debug; use thiserror::Error; use crate::relations::Relation; diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index c38edbd69..38bf99314 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -1,11 +1,10 @@ -use std::fmt::Debug; - use ark_ff::{Field, PrimeField}; use ark_r1cs_std::fields::fp::FpVar; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, }; use ark_std::{ + fmt::Debug, marker::PhantomData, ops::{Index, IndexMut}, }; From 7317892737a16f993a2f4b9d0b399a050c2701e8 Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 18 Nov 2025 04:30:45 +0800 Subject: [PATCH 10/93] Cleanup & reduce verbosity --- .../primitives/src/algebra/field/emulated.rs | 39 ++++--- crates/primitives/src/algebra/field/mod.rs | 7 +- .../primitives/src/algebra/group/emulated.rs | 4 +- crates/primitives/src/algebra/group/mod.rs | 5 +- crates/primitives/src/algebra/ops/poly.rs | 17 ++- .../src/arithmetizations/ccs/mod.rs | 42 +++---- crates/primitives/src/arithmetizations/mod.rs | 24 ++-- .../src/arithmetizations/r1cs/mod.rs | 50 +-------- crates/primitives/src/circuits/mod.rs | 3 +- crates/primitives/src/commitments/mod.rs | 19 ++-- crates/primitives/src/commitments/pedersen.rs | 104 ++++++++++++------ crates/primitives/src/relations/mod.rs | 19 ---- crates/primitives/src/sumcheck/mod.rs | 24 ++-- .../primitives/src/transcripts/griffin/mod.rs | 14 ++- .../src/transcripts/griffin/sponge.rs | 1 + crates/primitives/src/transcripts/mod.rs | 37 ++----- .../src/transcripts/poseidon/sponge.rs | 1 + 17 files changed, 202 insertions(+), 208 deletions(-) diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs index 73a5ec9cc..015140d01 100644 --- a/crates/primitives/src/algebra/field/emulated.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -100,8 +100,8 @@ pub struct IntVarInner { pub bounds: Vec, } -pub type BigIntVar = IntVarInner; -pub type EmulatedFieldVar = IntVarInner; +pub type BigIntVar = IntVarInner; +pub type EmulatedFieldVar = IntVarInner; impl GR1CSVar for IntVarInner { type Value = BigInt; @@ -125,14 +125,15 @@ impl GR1CSVar } fn value(&self) -> Result { - self.limbs.value().map(compose).map(|v| { - let (sign, abs) = v.into_parts(); - assert!(abs < Target::MODULUS.into()); - match sign { - Sign::Plus | Sign::NoSign => Target::from(abs), - Sign::Minus => Target::zero() - Target::from(abs), - } - }) + let v = compose(self.limbs.value()?); + let (sign, abs) = v.into_parts(); + if abs >= Target::MODULUS.into() { + return Err(SynthesisError::Unsatisfiable); + } + match sign { + Sign::Plus | Sign::NoSign => Ok(Target::from(abs)), + Sign::Minus => Ok(Target::zero() - Target::from(abs)), + } } } @@ -684,7 +685,9 @@ impl CondSelectGadget for IntVarInner ToBitsGadget for IntVarInner { fn to_bits_le(&self) -> Result>, SynthesisError> { for bound in &self.bounds { - assert!(bound.0 >= BigInt::zero()); + if bound.0 < BigInt::zero() { + return Err(SynthesisError::Unsatisfiable); + } } Ok(self .limbs @@ -946,6 +949,8 @@ impl AllocVar for IntVarInner IntVarInner { pub fn constant(x: BigInt) -> Self { + // `unwrap` below is safe because we are allocating a constant value, + // which is guaranteed to succeed. Self::new_constant(ConstraintSystemRef::None, (x.clone(), Bound(x.clone(), x))).unwrap() } } @@ -1128,7 +1133,7 @@ mod tests { Ok((a.clone(), Bound(lb.clone(), ub.clone()))) })?; - let a_const = BigIntVar::::constant(a.clone()); + let a_const = BigIntVar::::constant(a.clone()); assert_eq!(a, a_var.value()?); assert_eq!(a, a_const.value()?); @@ -1223,7 +1228,7 @@ mod tests { let aab = a * ab; let abb = ab * b; - let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; + let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; let b_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(b))?; let ab_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(ab))?; let aab_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(aab))?; @@ -1245,7 +1250,7 @@ mod tests { let a = Fq::rand(rng); - let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; + let a_var = EmulatedFieldVar::::new_witness(cs.clone(), || Ok(a))?; let mut r_var = a_var.clone(); for _ in 0..16 { @@ -1268,11 +1273,11 @@ mod tests { let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); - let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; - let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; + let a_var = Vec::>::new_witness(cs.clone(), || Ok(a))?; + let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; let c_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(c))?; - let mut r_var: EmulatedFieldVar = + let mut r_var: IntVarInner = EmulatedFieldVar::constant(BigUint::zero().into()).into(); for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { r_var = r_var.add_unaligned(&a.mul_unaligned(&b)?)?; diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs index 4cd35cd6e..5caac15b2 100644 --- a/crates/primitives/src/algebra/field/mod.rs +++ b/crates/primitives/src/algebra/field/mod.rs @@ -14,7 +14,10 @@ pub mod emulated; /// `Field` trait is a wrapper around `PrimeField` that also includes the /// necessary bounds for the field to be used conveniently in folding schemes. pub trait SonobeField: - PrimeField + Absorbable + Inputize + Val> + PrimeField + + Absorbable + + Inputize + + Val, EmulatedVar = EmulatedFieldVar> { const BITS_PER_LIMB: usize; } @@ -45,7 +48,7 @@ impl, const N: usize> Val for Fp { type ConstraintField = Self; type Var = FpVar; - type EmulatedVar = EmulatedFieldVar; + type EmulatedVar = EmulatedFieldVar; } impl, const N: usize> Absorbable for Fp { diff --git a/crates/primitives/src/algebra/group/emulated.rs b/crates/primitives/src/algebra/group/emulated.rs index 733dc2a45..4ebd0cf79 100644 --- a/crates/primitives/src/algebra/group/emulated.rs +++ b/crates/primitives/src/algebra/group/emulated.rs @@ -23,8 +23,8 @@ use crate::{ /// the affine coordinates in order to perform hash operations of the point. #[derive(Debug, Clone)] pub struct EmulatedAffineVar { - pub x: EmulatedFieldVar, - pub y: EmulatedFieldVar, + pub x: EmulatedFieldVar, + pub y: EmulatedFieldVar, } impl AllocVar diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs index 2dbf59ae5..c9ccf37d6 100644 --- a/crates/primitives/src/algebra/group/mod.rs +++ b/crates/primitives/src/algebra/group/mod.rs @@ -33,7 +33,10 @@ pub trait SonobeCurve: + Absorbable + Inputize + InputizeEmulated - + Val + AbsorbableGadget> + + Val< + Var: CurveVar + AbsorbableGadget, + EmulatedVar = EmulatedAffineVar + > { } diff --git a/crates/primitives/src/algebra/ops/poly.rs b/crates/primitives/src/algebra/ops/poly.rs index 53b5c345a..aec303013 100644 --- a/crates/primitives/src/algebra/ops/poly.rs +++ b/crates/primitives/src/algebra/ops/poly.rs @@ -1,10 +1,23 @@ -use ark_ff::PrimeField; -use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; +use ark_ff::{Field, PrimeField, Zero}; +use ark_poly::{DenseMultilinearExtension, EvaluationDomain, GeneralEvaluationDomain}; use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; use ark_relations::gr1cs::SynthesisError; +use ark_std::log2; use super::pow::Pow; +pub trait MLEHelper { + fn from_evaluations(evaluations: &[F]) -> Self; +} + +impl MLEHelper for DenseMultilinearExtension { + fn from_evaluations(evaluations: &[F]) -> Self { + let l = evaluations.len(); + let pad = vec![Zero::zero(); l.next_power_of_two() - l]; + Self::from_evaluations_vec(log2(l) as usize, [evaluations, &pad].concat()) + } +} + pub trait EvaluationDomainGadget { fn evaluate_all_lagrange_coefficients_var( &self, diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 6bed62774..a0fc6355a 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -1,19 +1,20 @@ use ark_ff::Field; use ark_poly::DenseMultilinearExtension; use ark_relations::gr1cs::{ConstraintSystem, Matrix}; -use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, fmt::Debug, log2, marker::PhantomData}; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, fmt::Debug, marker::PhantomData}; #[cfg(feature = "parallel")] use rayon::prelude::*; use super::{r1cs::R1CS, Arith, ArithRelation, Error}; use crate::{ + algebra::ops::poly::MLEHelper, arithmetizations::{r1cs::R1CSConfig, ArithConfig}, circuits::Assignments, }; pub mod circuits; -pub trait CCSVariant: Clone + Debug + PartialEq + Sync { +pub trait CCSVariant: Clone + Debug + PartialEq + Default + Sync { fn n_matrices() -> usize; fn degree() -> usize; @@ -24,7 +25,7 @@ pub trait CCSVariant: Clone + Debug + PartialEq + Sync { } #[allow(non_snake_case)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct CCSConfig { _v: PhantomData, /// m: number of rows in M_i (such that M_i \in F^{m, n}) @@ -36,16 +37,6 @@ pub struct CCSConfig { } impl ArithConfig for CCSConfig { - #[inline] - fn empty() -> Self { - Self { - _v: PhantomData, - m: 0, - n: 0, - l: 0, - } - } - #[inline] fn degree(&self) -> usize { V::degree() @@ -169,29 +160,30 @@ impl CCS { &self, z: Assignments + Sync>, ) -> Vec> { - let s = log2(self.n_constraints()) as usize; (0..V::n_matrices()) - .map(|i| DenseMultilinearExtension { - num_vars: s, - evaluations: cfg_iter!(self.M[i]) - .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) - .chain(vec![F::zero(); (1 << s) - self.n_constraints()]) - .collect(), + .map(|i| { + DenseMultilinearExtension::from_evaluations( + &cfg_iter!(self.M[i]) + .map(|row| row.iter().map(|(val, col)| z[*col] * val).sum()) + .collect::>(), + ) }) .collect() } } -impl Arith for CCS { - type Config = CCSConfig; - +impl Default for CCS { #[inline] - fn empty() -> Self { + fn default() -> Self { Self { - cfg: CCSConfig::empty(), + cfg: CCSConfig::default(), M: vec![vec![]; V::n_matrices()], } } +} + +impl Arith for CCS { + type Config = CCSConfig; #[inline] fn config(&self) -> &Self::Config { diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index 0866a7d11..f5f391209 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,5 +1,5 @@ use ark_relations::gr1cs::SynthesisError; -use ark_std::fmt::Debug; +use ark_std::{fmt::Debug, log2}; use thiserror::Error; use crate::relations::Relation; @@ -19,15 +19,17 @@ pub enum Error { SynthesisError(#[from] SynthesisError), } -pub trait ArithConfig: Clone + Debug + PartialEq { - fn empty() -> Self; - +pub trait ArithConfig: Clone + Debug + Default + PartialEq { /// Returns the degree of the constraint system fn degree(&self) -> usize; /// Returns the number of constraints in the constraint system fn n_constraints(&self) -> usize; + fn log_constraints(&self) -> usize { + log2(self.n_constraints()) as usize + } + /// Returns the number of variables in the constraint system fn n_variables(&self) -> usize; @@ -43,37 +45,45 @@ pub trait ArithConfig: Clone + Debug + PartialEq { /// [`Arith`] is a trait about constraint systems (R1CS, CCS, etc.), where we /// define methods for getting information about the constraint system. -pub trait Arith: Clone { +pub trait Arith: Clone + Default { type Config: ArithConfig; fn config(&self) -> &Self::Config; fn config_mut(&mut self) -> &mut Self::Config; - fn empty() -> Self; - /// Returns the degree of the constraint system + #[inline] fn degree(&self) -> usize { self.config().degree() } /// Returns the number of constraints in the constraint system + #[inline] fn n_constraints(&self) -> usize { self.config().n_constraints() } + #[inline] + fn log_constraints(&self) -> usize { + self.config().log_constraints() + } + /// Returns the number of variables in the constraint system + #[inline] fn n_variables(&self) -> usize { self.config().n_variables() } /// Returns the number of public inputs / public IO / instances / statements /// in the constraint system + #[inline] fn n_public_inputs(&self) -> usize { self.config().n_public_inputs() } /// Returns the number of witnesses / secret inputs in the constraint system + #[inline] fn n_witnesses(&self) -> usize { self.config().n_witnesses() } diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 2733a90fa..571ac6313 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -9,12 +9,11 @@ use super::{ccs::CCS, Arith, ArithRelation, Error}; use crate::{ arithmetizations::{ccs::CCSVariant, ArithConfig}, circuits::Assignments, - relations::WitnessInstanceExtractor, }; pub mod circuits; -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct R1CSConfig { m: usize, // number of constraints n: usize, // number of variables @@ -32,11 +31,6 @@ impl R1CSConfig { } impl ArithConfig for R1CSConfig { - #[inline] - fn empty() -> Self { - Self { m: 0, n: 0, l: 0 } - } - #[inline] fn degree(&self) -> usize { 2 @@ -97,7 +91,7 @@ impl CCSVariant for R1CSConfig { } #[allow(non_snake_case)] -#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct R1CS { cfg: R1CSConfig, pub A: Matrix, @@ -140,16 +134,6 @@ impl R1CS { impl Arith for R1CS { type Config = R1CSConfig; - #[inline] - fn empty() -> Self { - Self { - cfg: R1CSConfig::empty(), - A: vec![], - B: vec![], - C: vec![], - } - } - #[inline] fn config(&self) -> &Self::Config { &self.cfg @@ -217,15 +201,6 @@ impl, U: AsRef<[F]>> ArithRelation for R1CS { } } -impl WitnessInstanceExtractor, Vec> for R1CS { - type Source = Assignments>; - type Error = Error; - - fn extract(&self, z: Self::Source) -> Result<(Vec, Vec), Error> { - Ok((z.private, z.public)) - } -} - pub struct RelaxedWitness { pub w: V, pub e: V, @@ -262,22 +237,6 @@ impl ArithRelation, RelaxedInstance<&[F]>> for R1 } } -impl WitnessInstanceExtractor>, RelaxedInstance>> - for R1CS -{ - type Source = Assignments>; - type Error = Error; - - fn extract( - &self, - z: Self::Source, - ) -> Result<(RelaxedWitness>, RelaxedInstance>), Error> { - let (w, x) = self.extract(z)?; - let e = vec![F::zero(); self.n_constraints()]; - Ok((RelaxedWitness { w, e }, RelaxedInstance { x, u: F::one() })) - } -} - #[cfg(test)] pub mod tests { use ark_bn254::Fr; @@ -298,7 +257,7 @@ pub mod tests { x: Fr::rand(&mut rng), }; let cs = ConstraintSystem::new_ref(); - circuit.generate_constraints(cs.clone()).unwrap(); + circuit.generate_constraints(cs.clone())?; assert!(cs.is_satisfied()?); cs.finalize(); let cs = cs.into_inner().unwrap(); @@ -313,10 +272,9 @@ pub mod tests { let x = Fr::rand(&mut rng); let circuit = CircuitForTest:: { x }; let cs = ConstraintSystem::new_ref(); - circuit.generate_constraints(cs.clone()).unwrap(); + circuit.generate_constraints(cs.clone())?; assert!(cs.is_satisfied()?); cs.finalize(); - let cs = cs.into_inner().unwrap(); assert_eq!(cs.assignments()?, satisfying_assignments_for_test(x)); Ok(()) diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index 38bf99314..3bcdf6416 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -49,7 +49,6 @@ pub struct Assignments { } pub type AssignmentsOwned = Assignments>; -pub type AssignmentsRef<'a, F> = Assignments; impl From<(F, V, V)> for Assignments { fn from((u, x, w): (F, V, V)) -> Self { @@ -162,7 +161,7 @@ pub trait ConstraintSystemExt { fn assignments(&self) -> Result>, SynthesisError>; } -impl ConstraintSystemExt for ConstraintSystem { +impl ConstraintSystemExt for ConstraintSystemRef { fn assignments(&self) -> Result>, SynthesisError> { let witness = self.witness_assignment()?.to_vec(); // skip the first element which is '1' diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index 110a633f6..0c222042f 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,3 +1,4 @@ +use ark_ff::UniformRand; use ark_r1cs_std::{ alloc::AllocVar, eq::EqGadget, fields::fp::FpVar, select::CondSelectGadget, GR1CSVar, }; @@ -35,13 +36,17 @@ pub enum Error { CommitmentVerificationFail, } +pub trait CommitmentKey: Clone { + fn max_scalars_len(&self) -> usize; +} + pub trait VectorCommitment: 'static + Clone + Debug + PartialEq + Eq { const IS_HIDING: bool; type Gadget: VectorCommitmentGadget; - type Key: Clone; - type Scalar: Clone + Copy + Default + Debug + PartialEq + Eq + Sync + Absorbable; + type Key: CommitmentKey; + type Scalar: Clone + Copy + Default + Debug + PartialEq + Eq + Sync + Absorbable + UniformRand; type Commitment: Clone + Default + Debug + PartialEq + Eq + Sync + Absorbable; type Randomness: Clone + Copy @@ -58,7 +63,7 @@ pub trait VectorCommitment: 'static + Clone + Debug + PartialEq + Eq { + Mul + Sum; - fn generate_key(rng: impl RngCore, len: usize) -> Result; + fn generate_key(len: usize, rng: impl RngCore) -> Result; fn commit( ck: &Self::Key, @@ -71,7 +76,7 @@ pub trait VectorCommitment: 'static + Clone + Debug + PartialEq + Eq { v: &[Self::Scalar], r: &Self::Randomness, cm: &Self::Commitment, - ) -> Result; + ) -> Result<(), Error>; } pub trait GroupBasedVectorCommitment: @@ -79,7 +84,7 @@ pub trait GroupBasedVectorCommitment: Gadget: VectorCommitmentGadget< Native = Self, ConstraintField = CF2, - ScalarVar = EmulatedFieldVar, Self::Scalar, true>, + ScalarVar = EmulatedFieldVar, Self::Scalar>, CommitmentVar = Var, >, Commitment: SonobeCurve, @@ -151,9 +156,9 @@ mod tests { .map(|_| VC::Scalar::rand(&mut rng)) .collect::>(); - let ck = VC::generate_key(&mut rng, len)?; + let ck = VC::generate_key(len, &mut rng)?; let (cm, r) = VC::commit(&ck, &v, &mut rng)?; - assert!(VC::open(&ck, &v, &r, &cm)?); + VC::open(&ck, &v, &r, &cm)?; Ok(()) } } diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 4eb08956d..c3d4de26a 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -6,9 +6,12 @@ use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand use super::{Error, VectorCommitment}; use crate::{ - algebra::{field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar}, - commitments::{GroupBasedVectorCommitment, VectorCommitmentGadget}, - traits::{CF1, CF2, SonobeCurve}, + algebra::{ + field::emulated::{EmulatedFieldVar, IntVarInner}, + group::emulated::EmulatedAffineVar, + }, + commitments::{CommitmentKey, GroupBasedVectorCommitment, VectorCommitmentGadget}, + traits::{SonobeCurve, CF1, CF2}, utils::null::Null, }; @@ -17,14 +20,49 @@ pub struct Pedersen { _c: PhantomData, } -impl Pedersen { - fn msm(g: &[C::Affine], v: &[C::ScalarField]) -> Result { - if g.len() < v.len() { - return Err(Error::MessageTooLong(g.len(), v.len())); +#[derive(Clone)] +pub struct PedersenKey { + pub g: Vec, + pub h: C, +} + +impl CommitmentKey for PedersenKey { + fn max_scalars_len(&self) -> usize { + self.g.len() + } +} + +impl PedersenKey { + fn new(len: usize, mut rng: impl RngCore) -> Self { + let generators = repeat_with(|| C::rand(&mut rng)) + .take(len.next_power_of_two()) + .collect::>(); + Self { + g: C::normalize_batch(&generators), + h: if H { C::rand(&mut rng) } else { C::zero() }, + } + } +} + +impl PedersenKey { + fn commit(&self, v: &[C::ScalarField], r: &C::ScalarField) -> Result { + if self.g.len() < v.len() { + return Err(Error::MessageTooLong(self.g.len(), v.len())); + } + // + h * r + // use msm_unchecked because we already ensured at the if that generators are long enough + Ok(C::msm_unchecked(&self.g, v) + self.h.mul(r)) + } +} + +impl PedersenKey { + fn commit(&self, v: &[C::ScalarField]) -> Result { + if self.g.len() < v.len() { + return Err(Error::MessageTooLong(self.g.len(), v.len())); } // // use msm_unchecked because we already ensured at the if that generators are long enough - Ok(C::msm_unchecked(g, v)) + Ok(C::msm_unchecked(&self.g, v)) } } @@ -33,24 +71,21 @@ impl VectorCommitment for Pedersen { type Gadget = PedersenGadget; - type Key = Vec; + type Key = PedersenKey; type Scalar = C::ScalarField; type Commitment = C; type Randomness = Null; - fn generate_key(mut rng: impl RngCore, len: usize) -> Result { - let generators = repeat_with(|| C::rand(&mut rng)) - .take(len.next_power_of_two()) - .collect::>(); - Ok(C::normalize_batch(&generators)) + fn generate_key(len: usize, rng: impl RngCore) -> Result { + Ok(PedersenKey::new(len, rng)) } fn commit( - g: &Self::Key, + ck: &Self::Key, v: &[Self::Scalar], _rng: impl RngCore, ) -> Result<(Self::Commitment, Self::Randomness), Error> { - Ok((Self::msm(g, v)?, Null)) + Ok((ck.commit(v)?, Null)) } fn open( @@ -58,8 +93,10 @@ impl VectorCommitment for Pedersen { v: &[Self::Scalar], _r: &Self::Randomness, cm: &Self::Commitment, - ) -> Result { - Ok(&Self::msm(ck, v)? == cm) + ) -> Result<(), Error> { + (&ck.commit(v)? == cm) + .then_some(()) + .ok_or(Error::CommitmentVerificationFail) } } @@ -68,34 +105,33 @@ impl VectorCommitment for Pedersen { type Gadget = PedersenGadget; - type Key = (Vec, C); + type Key = PedersenKey; type Scalar = C::ScalarField; type Commitment = C; type Randomness = C::ScalarField; - fn generate_key(mut rng: impl RngCore, len: usize) -> Result { - Ok(( - Pedersen::::generate_key(&mut rng, len)?, - C::rand(&mut rng), - )) + fn generate_key(len: usize, rng: impl RngCore) -> Result { + Ok(PedersenKey::new(len, rng)) } fn commit( - (g, h): &Self::Key, + ck: &Self::Key, v: &[Self::Scalar], mut rng: impl RngCore, ) -> Result<(Self::Commitment, Self::Randomness), Error> { let r = C::ScalarField::rand(&mut rng); - Ok((Self::msm(g, v)? + h.mul(r), r)) + Ok((ck.commit(v, &r)?, r)) } fn open( - (g, h): &Self::Key, + ck: &Self::Key, v: &[Self::Scalar], r: &Self::Randomness, cm: &Self::Commitment, - ) -> Result { - Ok(&(Self::msm(g, v)? + h.mul(r)) == cm) + ) -> Result<(), Error> { + (&(ck.commit(v, r)?) == cm) + .then_some(()) + .ok_or(Error::CommitmentVerificationFail) } } @@ -228,9 +264,9 @@ impl VectorCommitmentGadget for PedersenGadget { type KeyVar = Vec; - type ScalarVar = EmulatedFieldVar, CF1, true>; + type ScalarVar = EmulatedFieldVar, CF1>; - type IntermediateScalarVar = EmulatedFieldVar, CF1, false>; + type IntermediateScalarVar = IntVarInner, CF1, false>; type CommitmentVar = C::Var; @@ -258,13 +294,13 @@ impl VectorCommitmentGadget for PedersenGadget { type KeyVar = (Vec, C::Var); - type ScalarVar = EmulatedFieldVar, CF1, true>; + type ScalarVar = EmulatedFieldVar, CF1>; - type IntermediateScalarVar = EmulatedFieldVar, CF1, false>; + type IntermediateScalarVar = IntVarInner, CF1, false>; type CommitmentVar = C::Var; - type RandomnessVar = EmulatedFieldVar, CF1, true>; + type RandomnessVar = EmulatedFieldVar, CF1>; fn open( (g, h): &Self::KeyVar, diff --git a/crates/primitives/src/relations/mod.rs b/crates/primitives/src/relations/mod.rs index 6c3241700..0e3467057 100644 --- a/crates/primitives/src/relations/mod.rs +++ b/crates/primitives/src/relations/mod.rs @@ -1,7 +1,5 @@ use ark_std::{error::Error, rand::RngCore}; -use crate::traits::Dummy; - pub trait Relation { type Error: Error; @@ -9,23 +7,6 @@ pub trait Relation { fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error>; } -pub trait WitnessInstanceExtractor { - type Source; - type Error: Error + 'static; - - fn extract(&self, source: Self::Source) -> Result<(W, U), Self::Error>; -} - -pub trait WitnessInstanceInitializer { - fn dummy_witness_instance<'a>(&'a self) -> (W, U) - where - W: Dummy<&'a Self>, - U: Dummy<&'a Self>, - { - (W::dummy(self), U::dummy(self)) - } -} - /// `WitnessInstanceSampler` allows sampling a random witness-instance pair that /// satisfies the relation `self`. pub trait WitnessInstanceSampler { diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index 4b32465ad..610102b4e 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -29,8 +29,8 @@ pub mod utils; #[derive(Debug, Error)] pub enum Error { - #[error("Incorrect evaluations: {0} + {1} != {2}")] - IncorrectEvaluations(String, String, String), + #[error("Incorrect evaluation: claimed {0}, got {1}")] + IncorrectEvaluation(String, String), #[error("Incorrect proof length: expected {0}, got {1}")] UnexpectedProofLength(usize, usize), #[error("Unexpected polynomial degree: expected at most {0}, got {1}")] @@ -155,7 +155,7 @@ impl IOPSumCheck { } pub fn verify( - claimed_sum: F, + mut claimed_sum: F, proofs: &[Vec], aux_info: &VPAuxInfo, transcript: &mut impl Transcript, @@ -170,7 +170,6 @@ impl IOPSumCheck { } let mut challenges = Vec::with_capacity(aux_info.num_variables); - let mut expected = claimed_sum; // Outer loop is not parallelized because `DensePolynomial::evaluate` is // already parallelized internally. @@ -186,12 +185,11 @@ impl IOPSumCheck { let eval_at_one = coeffs.iter().sum::(); // the deferred check during the interactive phase: - // 1. check if the received 'P(0) + P(1) = expected`. - if eval_at_zero + eval_at_one != expected { - return Err(Error::IncorrectEvaluations( - eval_at_zero.to_string(), - eval_at_one.to_string(), - expected.to_string(), + // 1. check if the received 'P(0) + P(1) = claimed_sum`. + if eval_at_zero + eval_at_one != claimed_sum { + return Err(Error::IncorrectEvaluation( + claimed_sum.to_string(), + format!("{} + {}", eval_at_zero, eval_at_one), )); } @@ -199,11 +197,11 @@ impl IOPSumCheck { let challenge = transcript.challenge_field_element(); // 2. set `expected` to `P(r)` - expected = DensePolynomial::from_coefficients_slice(coeffs).evaluate(&challenge); + claimed_sum = DensePolynomial::from_coefficients_slice(coeffs).evaluate(&challenge); challenges.push(challenge); } - Ok((expected, challenges)) + Ok((claimed_sum, challenges)) } } @@ -212,7 +210,7 @@ pub mod tests { use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; use ark_ff::Field; use ark_pallas::Fr; - use ark_poly::{DenseMultilinearExtension, MultilinearExtension}; + use ark_poly::MultilinearExtension; use ark_std::{test_rng, One, Zero}; use super::*; diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs index a16300dba..b4c931d92 100644 --- a/crates/primitives/src/transcripts/griffin/mod.rs +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -455,12 +455,12 @@ mod tests { use ark_bn254::Fr; use ark_ff::UniformRand; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::rand::thread_rng; + use ark_std::{error::Error, rand::thread_rng}; use super::*; #[test] - fn test() { + fn test() -> Result<(), Box> { let rng = &mut thread_rng(); let griffin = GriffinParams::new(24, 5, 9); let t = griffin.t; @@ -469,11 +469,13 @@ mod tests { let y = griffin.hash(&x); let cs = ConstraintSystem::new_ref(); - let x_var = Vec::new_witness(cs.clone(), || Ok(x.clone())).unwrap(); - let y_var = griffin.hash_gadget(&x_var).unwrap(); - assert_eq!(y, y_var.value().unwrap()); + let x_var = Vec::new_witness(cs.clone(), || Ok(x.clone()))?; + let y_var = griffin.hash_gadget(&x_var)?; + assert_eq!(y, y_var.value()?); println!("{}", cs.num_constraints()); - assert!(cs.is_satisfied().unwrap()); + assert!(cs.is_satisfied()?); + + Ok(()) } } diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs index 314486d68..c290a25ca 100644 --- a/crates/primitives/src/transcripts/griffin/sponge.rs +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -184,6 +184,7 @@ impl GriffinSpongeVar { impl Transcript for GriffinSponge { type Config = Arc>; + type Var = GriffinSpongeVar; fn new(parameters: &Arc>) -> Self { let state = vec![F::zero(); parameters.rate + parameters.capacity]; diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index 4b6820a04..7f5ebc8e3 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -7,17 +7,15 @@ pub mod absorbable; pub mod griffin; pub mod poseidon; -pub trait Transcript { - type Config; +pub trait Transcript: Clone { + type Config: Clone; + type Var: TranscriptVar; fn new(config: &Self::Config) -> Self; /// `new_with_pp_hash` creates a new transcript / sponge with the given /// hash of the public parameters. - fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self - where - Self: Sized, - { + fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self { let mut sponge = Self::new(config); sponge.add_field_elements(&[pp_hash]); sponge @@ -41,10 +39,7 @@ pub trait Transcript { fn get_field_elements(&mut self, num_elements: usize) -> Vec; /// Creates a new sponge with applied domain separation. - fn separate_domain(&self, domain: &[u8]) -> Self - where - Self: Clone, - { + fn separate_domain(&self, domain: &[u8]) -> Self { let mut new_sponge = self.clone(); let mut input = domain.len().to_le_bytes().to_vec(); @@ -85,22 +80,17 @@ pub trait Transcript { } } -pub trait TranscriptVar { - type Native: Transcript; +pub trait TranscriptVar: Clone { + type Native: Transcript; - fn new(config: &>::Config) -> Self - where - Self: Sized; + fn new(config: &>::Config) -> Self; /// `new_with_pp_hash` creates a new transcript / sponge with the given /// hash of the public parameters. fn new_with_pp_hash( config: &>::Config, pp_hash: &FpVar, - ) -> Result - where - Self: Sized, - { + ) -> Result { let mut sponge = Self::new(config); sponge.add(&pp_hash)?; Ok(sponge) @@ -115,16 +105,13 @@ pub trait TranscriptVar { fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError>; fn get_field_element(&mut self) -> Result, SynthesisError> { - Ok(self.get_field_elements(1)?.pop().unwrap()) + Ok(self.get_field_elements(1)?.swap_remove(0)) } fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError>; /// Creates a new sponge with applied domain separation. - fn separate_domain(&self, domain: &[u8]) -> Result - where - Self: Clone, - { + fn separate_domain(&self, domain: &[u8]) -> Result { let mut new_sponge = self.clone(); let mut input = domain.len().to_le_bytes().to_vec(); @@ -143,7 +130,7 @@ pub trait TranscriptVar { fn challenge_field_element(&mut self) -> Result, SynthesisError> { let mut c = self.get_field_elements(1)?; self.add(&c[0])?; - Ok(c.pop().unwrap()) + Ok(c.swap_remove(0)) } fn challenge_bits(&mut self, nbits: usize) -> Result>, SynthesisError> { diff --git a/crates/primitives/src/transcripts/poseidon/sponge.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs index 9601d8bbe..a9123a20b 100644 --- a/crates/primitives/src/transcripts/poseidon/sponge.rs +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -12,6 +12,7 @@ use crate::transcripts::{AbsorbableGadget, Transcript, TranscriptVar}; impl Transcript for PoseidonSponge { type Config = PoseidonConfig; + type Var = PoseidonSpongeVar; fn new(config: &Self::Config) -> Self { CryptographicSponge::new(config) From a3ad72bc5bf5614bc7ddbed28312612551a9c258 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 22:33:18 +0800 Subject: [PATCH 11/93] Allow step circuit to have states of any shape --- crates/primitives/src/circuits/mod.rs | 21 ++++++++++++++------- crates/primitives/src/circuits/utils.rs | 22 ++++++++++++---------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index 3bcdf6416..845dd2924 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -1,5 +1,5 @@ use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::fields::fp::FpVar; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, GR1CSVar}; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, }; @@ -9,6 +9,11 @@ use ark_std::{ ops::{Index, IndexMut}, }; +use crate::{ + traits::Dummy, + transcripts::{Absorbable, AbsorbableGadget}, +}; + pub mod utils; /// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie. @@ -21,13 +26,15 @@ pub mod utils; /// contains a vector, it is initialized at the expected length). pub trait FCircuit { type Field: PrimeField; + type State: Clone + PartialEq + Absorbable; + type StateVar: GR1CSVar + + AllocVar + + AbsorbableGadget; type ExternalInputs; - fn dummy_external_inputs(&self) -> Self::ExternalInputs; + fn dummy_state(&self) -> Self::State; - /// returns the number of elements in the state of the FCircuit, which corresponds to the - /// FCircuit inputs. - fn state_len(&self) -> usize; + fn dummy_external_inputs(&self) -> Self::ExternalInputs; /// generates the constraints for the step of F for the given z_i fn generate_step_constraints( @@ -36,9 +43,9 @@ pub trait FCircuit { &self, cs: ConstraintSystemRef, i: FpVar, - z_i: Vec>, + z_i: Self::StateVar, external_inputs: Self::ExternalInputs, // inputs that are not part of the state - ) -> Result>, SynthesisError>; + ) -> Result; } #[derive(Clone, Debug, PartialEq)] diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs index 304c2119f..dccbe34a1 100644 --- a/crates/primitives/src/circuits/utils.rs +++ b/crates/primitives/src/circuits/utils.rs @@ -7,8 +7,8 @@ use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, Synthesis use super::Assignments; use crate::{ - arithmetizations::r1cs::{R1CSConfig, R1CS}, - circuits::FCircuit, + arithmetizations::r1cs::{R1CS, R1CSConfig}, + circuits::FCircuit, traits::SonobeField, }; pub struct CircuitForTest { @@ -47,24 +47,26 @@ impl ConstraintSynthesizer for CircuitForTest { } } -impl FCircuit for CircuitForTest { +impl FCircuit for CircuitForTest { type Field = F; + type State = [F; 1]; + type StateVar = [FpVar; 1]; type ExternalInputs = (); - fn dummy_external_inputs(&self) -> Self::ExternalInputs {} - - fn state_len(&self) -> usize { - 1 + fn dummy_state(&self) -> Self::State { + [F::zero(); 1] } + fn dummy_external_inputs(&self) -> Self::ExternalInputs {} + fn generate_step_constraints( &self, cs: ConstraintSystemRef, _i: FpVar, - z_i: Vec>, + z_i: Self::StateVar, _external_inputs: Self::ExternalInputs, - ) -> Result>, SynthesisError> { + ) -> Result { // Variable 0 (implicitly added by arkworks as 1) // Variable 1 let x = if let FpVar::Var(x) = z_i[0].clone() { @@ -97,7 +99,7 @@ impl FCircuit for CircuitForTest { || Variable::one().into(), || y.variable.into(), )?; - Ok(vec![FpVar::Var(x_cube_plus_x_plus_5)]) + Ok([FpVar::Var(x_cube_plus_x_plus_5)]) } } From 5f693fdf89d56180dd401129e8192c912cc48d0f Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 22 Nov 2025 05:40:41 +0800 Subject: [PATCH 12/93] Separate VC and FS into Def and Ops --- crates/primitives/src/commitments/mod.rs | 86 +++--- crates/primitives/src/commitments/pedersen.rs | 264 ++++++++---------- 2 files changed, 176 insertions(+), 174 deletions(-) diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index 0c222042f..64abea149 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -13,8 +13,10 @@ use thiserror::Error; use crate::{ algebra::{ - field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar, - ops::bits::FromBitsGadget, Var, + field::emulated::{EmulatedFieldVar, IntVarInner}, + group::emulated::EmulatedAffineVar, + ops::bits::FromBitsGadget, + Var, }, traits::{SonobeCurve, SonobeField, CF1, CF2}, transcripts::{Absorbable, AbsorbableGadget}, @@ -40,11 +42,9 @@ pub trait CommitmentKey: Clone { fn max_scalars_len(&self) -> usize; } -pub trait VectorCommitment: 'static + Clone + Debug + PartialEq + Eq { +pub trait VectorCommitmentDef: 'static + Clone + Debug + PartialEq + Eq { const IS_HIDING: bool; - type Gadget: VectorCommitmentGadget; - type Key: CommitmentKey; type Scalar: Clone + Copy + Default + Debug + PartialEq + Eq + Sync + Absorbable + UniformRand; type Commitment: Clone + Default + Debug + PartialEq + Eq + Sync + Absorbable; @@ -62,7 +62,9 @@ pub trait VectorCommitment: 'static + Clone + Debug + PartialEq + Eq { + Add + Mul + Sum; +} +pub trait VectorCommitmentOps: VectorCommitmentDef { fn generate_key(len: usize, rng: impl RngCore) -> Result; fn commit( @@ -79,28 +81,7 @@ pub trait VectorCommitment: 'static + Clone + Debug + PartialEq + Eq { ) -> Result<(), Error>; } -pub trait GroupBasedVectorCommitment: - VectorCommitment< - Gadget: VectorCommitmentGadget< - Native = Self, - ConstraintField = CF2, - ScalarVar = EmulatedFieldVar, Self::Scalar>, - CommitmentVar = Var, - >, - Commitment: SonobeCurve, - Scalar = CF1<::Commitment>, -> -{ - type EmulatedGadget: VectorCommitmentGadget< - Native = Self, - ConstraintField = Self::Scalar, - ScalarVar = FpVar, - CommitmentVar = EmulatedAffineVar, - >; -} - -pub trait VectorCommitmentGadget: Clone { - type Native: VectorCommitment; +pub trait VectorCommitmentGadgetDef: Clone { type ConstraintField: SonobeField; type KeyVar; @@ -109,8 +90,8 @@ pub trait VectorCommitmentGadget: Clone { + AbsorbableGadget + CondSelectGadget + FromBitsGadget - + AllocVar<::Scalar, Self::ConstraintField> - + GR1CSVar::Scalar> + + AllocVar<::Scalar, Self::ConstraintField> + + GR1CSVar::Scalar> + Add + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> + Mul @@ -128,11 +109,15 @@ pub trait VectorCommitmentGadget: Clone { type CommitmentVar: Clone + AbsorbableGadget + CondSelectGadget - + AllocVar<::Commitment, Self::ConstraintField> - + GR1CSVar::Commitment>; - type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> - + GR1CSVar::Randomness>; + + AllocVar<::Commitment, Self::ConstraintField> + + GR1CSVar::Commitment>; + type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> + + GR1CSVar::Randomness>; + type Native: VectorCommitmentDef; +} + +pub trait VectorCommitmentGadgetOps: VectorCommitmentGadgetDef { fn open( ck: &Self::KeyVar, v: &[Self::ScalarVar], @@ -141,6 +126,39 @@ pub trait VectorCommitmentGadget: Clone { ) -> Result<(), SynthesisError>; } +pub trait GroupBasedVectorCommitment: + VectorCommitmentDef< + Commitment: SonobeCurve, + Scalar = CF1<::Commitment>, + > + VectorCommitmentOps +{ + type Gadget1: VectorCommitmentGadgetOps + + VectorCommitmentGadgetDef< + ConstraintField = CF2<::Commitment>, + ScalarVar = EmulatedFieldVar< + CF2<::Commitment>, + ::Scalar, + >, + IntermediateScalarVar = IntVarInner< + CF2<::Commitment>, + ::Scalar, + false, + >, + CommitmentVar = Var<::Commitment>, + Native = Self, + >; + type Gadget2: VectorCommitmentGadgetDef< + ConstraintField = ::Scalar, + ScalarVar = FpVar<::Scalar>, + IntermediateScalarVar = FpVar<::Scalar>, + CommitmentVar = EmulatedAffineVar< + ::Scalar, + ::Commitment, + >, + Native = Self, + >; +} + #[cfg(test)] mod tests { use ark_ff::UniformRand; @@ -148,7 +166,7 @@ mod tests { use super::*; - pub fn test_commitment_correctness>( + pub fn test_commitment_correctness( mut rng: impl RngCore, len: usize, ) -> Result<(), Box> { diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index c3d4de26a..5ec639358 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -4,22 +4,19 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::SynthesisError; use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; -use super::{Error, VectorCommitment}; +use super::{ + CommitmentKey, Error, VectorCommitmentDef, VectorCommitmentGadgetDef, VectorCommitmentOps, +}; use crate::{ algebra::{ field::emulated::{EmulatedFieldVar, IntVarInner}, group::emulated::EmulatedAffineVar, }, - commitments::{CommitmentKey, GroupBasedVectorCommitment, VectorCommitmentGadget}, + commitments::{GroupBasedVectorCommitment, VectorCommitmentGadgetOps}, traits::{SonobeCurve, CF1, CF2}, utils::null::Null, }; -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Pedersen { - _c: PhantomData, -} - #[derive(Clone)] pub struct PedersenKey { pub g: Vec, @@ -66,83 +63,152 @@ impl PedersenKey { } } -impl VectorCommitment for Pedersen { - const IS_HIDING: bool = false; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Pedersen { + _c: PhantomData, +} - type Gadget = PedersenGadget; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PedersenEmulatedGadget { + _c: PhantomData, +} + +impl VectorCommitmentDef for Pedersen { + const IS_HIDING: bool = false; type Key = PedersenKey; type Scalar = C::ScalarField; type Commitment = C; type Randomness = Null; +} + +impl VectorCommitmentDef for Pedersen { + const IS_HIDING: bool = true; + + type Key = PedersenKey; + type Scalar = C::ScalarField; + type Commitment = C; + type Randomness = C::ScalarField; +} + +impl VectorCommitmentGadgetDef for PedersenGadget { + type ConstraintField = CF2; + + type KeyVar = Vec; + + type ScalarVar = EmulatedFieldVar, CF1>; - fn generate_key(len: usize, rng: impl RngCore) -> Result { + type IntermediateScalarVar = IntVarInner, CF1, false>; + + type CommitmentVar = C::Var; + + type RandomnessVar = Null; + + type Native = Pedersen; +} + +impl VectorCommitmentGadgetDef for PedersenGadget { + type ConstraintField = CF2; + + type KeyVar = (Vec, C::Var); + + type ScalarVar = EmulatedFieldVar, CF1>; + + type IntermediateScalarVar = IntVarInner, CF1, false>; + + type CommitmentVar = C::Var; + + type RandomnessVar = EmulatedFieldVar, CF1>; + + type Native = Pedersen; +} + +impl VectorCommitmentGadgetDef for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = Vec, C>>; + + type ScalarVar = FpVar>; + + type IntermediateScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = Null; + + type Native = Pedersen; +} + +impl VectorCommitmentGadgetDef for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = ( + Vec, C>>, + EmulatedAffineVar, C>, + ); + + type ScalarVar = FpVar>; + + type IntermediateScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = FpVar>; + + type Native = Pedersen; +} + +impl GroupBasedVectorCommitment for Pedersen { + type Gadget1 = PedersenGadget; + type Gadget2 = PedersenEmulatedGadget; +} + +impl GroupBasedVectorCommitment for Pedersen { + type Gadget1 = PedersenGadget; + type Gadget2 = PedersenEmulatedGadget; +} + +impl VectorCommitmentOps for Pedersen { + fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { Ok(PedersenKey::new(len, rng)) } fn commit( - ck: &Self::Key, - v: &[Self::Scalar], + ck: &PedersenKey, + v: &[CF1], _rng: impl RngCore, - ) -> Result<(Self::Commitment, Self::Randomness), Error> { + ) -> Result<(C, Null), Error> { Ok((ck.commit(v)?, Null)) } - fn open( - ck: &Self::Key, - v: &[Self::Scalar], - _r: &Self::Randomness, - cm: &Self::Commitment, - ) -> Result<(), Error> { + fn open(ck: &PedersenKey, v: &[CF1], _r: &Null, cm: &C) -> Result<(), Error> { (&ck.commit(v)? == cm) .then_some(()) .ok_or(Error::CommitmentVerificationFail) } } -impl VectorCommitment for Pedersen { - const IS_HIDING: bool = true; - - type Gadget = PedersenGadget; - - type Key = PedersenKey; - type Scalar = C::ScalarField; - type Commitment = C; - type Randomness = C::ScalarField; - - fn generate_key(len: usize, rng: impl RngCore) -> Result { +impl VectorCommitmentOps for Pedersen { + fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { Ok(PedersenKey::new(len, rng)) } fn commit( - ck: &Self::Key, - v: &[Self::Scalar], + ck: &PedersenKey, + v: &[CF1], mut rng: impl RngCore, - ) -> Result<(Self::Commitment, Self::Randomness), Error> { + ) -> Result<(C, CF1), Error> { let r = C::ScalarField::rand(&mut rng); Ok((ck.commit(v, &r)?, r)) } - fn open( - ck: &Self::Key, - v: &[Self::Scalar], - r: &Self::Randomness, - cm: &Self::Commitment, - ) -> Result<(), Error> { + fn open(ck: &PedersenKey, v: &[CF1], r: &CF1, cm: &C) -> Result<(), Error> { (&(ck.commit(v, r)?) == cm) .then_some(()) .ok_or(Error::CommitmentVerificationFail) } } -impl GroupBasedVectorCommitment for Pedersen { - type EmulatedGadget = PedersenEmulatedGadget; -} - -impl GroupBasedVectorCommitment for Pedersen { - type EmulatedGadget = PedersenEmulatedGadget; -} - #[derive(Clone)] pub struct PedersenGadget { _c: PhantomData, @@ -258,25 +324,12 @@ impl PedersenGadget { } } -impl VectorCommitmentGadget for PedersenGadget { - type Native = Pedersen; - type ConstraintField = CF2; - - type KeyVar = Vec; - - type ScalarVar = EmulatedFieldVar, CF1>; - - type IntermediateScalarVar = IntVarInner, CF1, false>; - - type CommitmentVar = C::Var; - - type RandomnessVar = Null; - +impl VectorCommitmentGadgetOps for PedersenGadget { fn open( - ck: &Self::KeyVar, - v: &[Self::ScalarVar], - _r: &Self::RandomnessVar, - cm: &Self::CommitmentVar, + ck: &Vec, + v: &[EmulatedFieldVar, CF1>], + _r: &Null, + cm: &C::Var, ) -> Result<(), SynthesisError> { Self::msm( ck, @@ -288,25 +341,12 @@ impl VectorCommitmentGadget for PedersenGadget { } } -impl VectorCommitmentGadget for PedersenGadget { - type Native = Pedersen; - type ConstraintField = CF2; - - type KeyVar = (Vec, C::Var); - - type ScalarVar = EmulatedFieldVar, CF1>; - - type IntermediateScalarVar = IntVarInner, CF1, false>; - - type CommitmentVar = C::Var; - - type RandomnessVar = EmulatedFieldVar, CF1>; - +impl VectorCommitmentGadgetOps for PedersenGadget { fn open( - (g, h): &Self::KeyVar, - v: &[Self::ScalarVar], - r: &Self::RandomnessVar, - cm: &Self::CommitmentVar, + (g, h): &(Vec, C::Var), + v: &[EmulatedFieldVar, CF1>], + r: &EmulatedFieldVar, CF1>, + cm: &C::Var, ) -> Result<(), SynthesisError> { let gv = Self::msm( g, @@ -319,62 +359,6 @@ impl VectorCommitmentGadget for PedersenGadget { } } -#[derive(Clone)] -pub struct PedersenEmulatedGadget { - _c: PhantomData, -} - -impl VectorCommitmentGadget for PedersenEmulatedGadget { - type Native = Pedersen; - type ConstraintField = CF1; - - type KeyVar = Vec, C>>; - - type ScalarVar = FpVar>; - - type IntermediateScalarVar = FpVar>; - - type CommitmentVar = EmulatedAffineVar, C>; - - type RandomnessVar = Null; - - fn open( - ck: &Self::KeyVar, - v: &[Self::ScalarVar], - _r: &Self::RandomnessVar, - cm: &Self::CommitmentVar, - ) -> Result<(), SynthesisError> { - unimplemented!() - } -} - -impl VectorCommitmentGadget for PedersenEmulatedGadget { - type Native = Pedersen; - type ConstraintField = CF1; - - type KeyVar = ( - Vec, C>>, - EmulatedAffineVar, C>, - ); - - type ScalarVar = FpVar>; - - type IntermediateScalarVar = FpVar>; - - type CommitmentVar = EmulatedAffineVar, C>; - - type RandomnessVar = FpVar>; - - fn open( - (g, h): &Self::KeyVar, - v: &[Self::ScalarVar], - r: &Self::RandomnessVar, - cm: &Self::CommitmentVar, - ) -> Result<(), SynthesisError> { - unimplemented!() - } -} - #[cfg(test)] mod tests { use ark_bn254::G1Projective; From 3e8206b3e85aa78bafa524087245c0e88a561f3d Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 24 Nov 2025 03:11:21 +0800 Subject: [PATCH 13/93] Cleanup --- crates/primitives/src/algebra/group/mod.rs | 2 -- crates/primitives/src/algebra/ops/bits.rs | 10 +++++++ .../src/arithmetizations/ccs/mod.rs | 10 +++---- .../src/arithmetizations/r1cs/mod.rs | 5 +++- crates/primitives/src/circuits/mod.rs | 5 +--- crates/primitives/src/commitments/mod.rs | 26 ++++++------------- .../src/transcripts/griffin/sponge.rs | 4 +-- .../src/transcripts/poseidon/sponge.rs | 8 +++--- 8 files changed, 33 insertions(+), 37 deletions(-) diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs index c9ccf37d6..a429f8f2e 100644 --- a/crates/primitives/src/algebra/group/mod.rs +++ b/crates/primitives/src/algebra/group/mod.rs @@ -23,8 +23,6 @@ pub mod emulated; pub type CF1 = ::ScalarField; pub type CF2 = <::BaseField as Field>::BasePrimeField; -pub type CI1 = <::ScalarField as PrimeField>::BigInt; -pub type CI2 = <<::BaseField as Field>::BasePrimeField as PrimeField>::BigInt; /// `Curve` trait is a wrapper around `CurveGroup` that also includes the /// necessary bounds for the curve to be used conveniently in folding schemes. diff --git a/crates/primitives/src/algebra/ops/bits.rs b/crates/primitives/src/algebra/ops/bits.rs index 1e431730b..12a0c5dee 100644 --- a/crates/primitives/src/algebra/ops/bits.rs +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -4,6 +4,16 @@ use ark_relations::gr1cs::SynthesisError; use crate::algebra::field::emulated::Bound; +pub trait FromBits { + fn from_bits_le(bits: &[bool]) -> Self; +} + +impl FromBits for F { + fn from_bits_le(bits: &[bool]) -> Self { + F::from(F::BigInt::from_bits_le(bits)) + } +} + pub trait FromBitsGadget: Sized { fn from_bits_le(bits: &[Boolean], bound: Bound) -> Result; } diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index a0fc6355a..83424f422 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -116,9 +116,6 @@ impl CCS { )); } - let S = &V::multisets_vec(); - let c = &V::coefficients_vec::(); - // Recall that the evaluation of CCS at z is defined as: // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i * z))$, // where $\prod$ denotes the Hadamard product. @@ -134,9 +131,10 @@ impl CCS { .map(|row| { // The row-th entry of the resulting vector is: // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i[row] * z))$ - S.iter() - .zip(c) - .map(|(s, &c)| { + V::multisets_vec() + .into_iter() + .zip(V::coefficients_vec::()) + .map(|(s, c)| { // Each term in the sum is: // $c_j * \prod_{i \in S_j} (M_i[row] * z)$ c * s diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 571ac6313..1def67afc 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -1,6 +1,5 @@ use ark_ff::Field; use ark_relations::gr1cs::{ConstraintSystem, Matrix, R1CS_PREDICATE_LABEL}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::{cfg_into_iter, cfg_iter, iterable::Iterable}; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -73,18 +72,22 @@ impl From<&ConstraintSystem> for R1CSConfig { } impl CCSVariant for R1CSConfig { + #[inline] fn n_matrices() -> usize { 3 } + #[inline] fn degree() -> usize { 2 } + #[inline] fn multisets_vec() -> Vec> { vec![vec![0, 1], vec![2]] } + #[inline] fn coefficients_vec() -> Vec { vec![F::one(), -F::one()] } diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index 845dd2924..ca0b2967b 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -9,10 +9,7 @@ use ark_std::{ ops::{Index, IndexMut}, }; -use crate::{ - traits::Dummy, - transcripts::{Absorbable, AbsorbableGadget}, -}; +use crate::transcripts::{Absorbable, AbsorbableGadget}; pub mod utils; diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index 64abea149..816c0b4be 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -134,27 +134,17 @@ pub trait GroupBasedVectorCommitment: { type Gadget1: VectorCommitmentGadgetOps + VectorCommitmentGadgetDef< - ConstraintField = CF2<::Commitment>, - ScalarVar = EmulatedFieldVar< - CF2<::Commitment>, - ::Scalar, - >, - IntermediateScalarVar = IntVarInner< - CF2<::Commitment>, - ::Scalar, - false, - >, - CommitmentVar = Var<::Commitment>, + ConstraintField = CF2, + ScalarVar = EmulatedFieldVar, Self::Scalar>, + IntermediateScalarVar = IntVarInner, Self::Scalar, false>, + CommitmentVar = Var, Native = Self, >; type Gadget2: VectorCommitmentGadgetDef< - ConstraintField = ::Scalar, - ScalarVar = FpVar<::Scalar>, - IntermediateScalarVar = FpVar<::Scalar>, - CommitmentVar = EmulatedAffineVar< - ::Scalar, - ::Commitment, - >, + ConstraintField = Self::Scalar, + ScalarVar = FpVar, + IntermediateScalarVar = FpVar, + CommitmentVar = EmulatedAffineVar, Native = Self, >; } diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs index c290a25ca..28d89efa7 100644 --- a/crates/primitives/src/transcripts/griffin/sponge.rs +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -363,7 +363,7 @@ pub mod tests { use ark_std::{error::Error, test_rng}; use super::*; - use crate::algebra::group::emulated::EmulatedAffineVar; + use crate::algebra::{group::emulated::EmulatedAffineVar, ops::bits::FromBits}; #[test] fn test_transcript_and_transcriptvar_absorb_native_point() -> Result<(), Box> { @@ -457,7 +457,7 @@ pub mod tests { // multiply point P by the challenge in different formats, to ensure that we get the same // result natively and in-circuit - let c = Fr::from(::BigInt::from_bits_le(&c_bits)); + let c = Fr::from_bits_le(&c_bits); // check that native c*P and in-circuit c*P using scalar_mul_le are equal assert_eq!(p * c, p_var.scalar_mul_le(c_var.iter())?.value()?); diff --git a/crates/primitives/src/transcripts/poseidon/sponge.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs index a9123a20b..f1f58cc76 100644 --- a/crates/primitives/src/transcripts/poseidon/sponge.rs +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -79,7 +79,7 @@ pub mod tests { use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; use ark_crypto_primitives::sponge::poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}; use ark_ec::PrimeGroup; - use ark_ff::{BigInteger, PrimeField, UniformRand}; + use ark_ff::UniformRand; use ark_r1cs_std::{ alloc::AllocVar, fields::fp::FpVar, @@ -90,8 +90,8 @@ pub mod tests { use ark_std::{error::Error, str::FromStr, test_rng}; use crate::{ - algebra::group::emulated::EmulatedAffineVar, - transcripts::{poseidon::poseidon_canonical_config, Transcript, TranscriptVar}, + algebra::{group::emulated::EmulatedAffineVar, ops::bits::FromBits}, + transcripts::{Transcript, TranscriptVar, poseidon::poseidon_canonical_config}, }; // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 @@ -207,7 +207,7 @@ pub mod tests { // multiply point P by the challenge in different formats, to ensure that we get the same // result natively and in-circuit - let c = Fr::from(::BigInt::from_bits_le(&c_bits)); + let c = Fr::from_bits_le(&c_bits); // check that native c*P and in-circuit c*P using scalar_mul_le are equal assert_eq!(p * c, p_var.scalar_mul_le(c_var.iter())?.value()?); From f7505487dae61e8ea5501d12963ef68502ef861e Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 24 Nov 2025 05:30:24 +0800 Subject: [PATCH 14/93] FCircuit now allows external outputs --- crates/primitives/src/circuits/mod.rs | 3 ++- crates/primitives/src/circuits/utils.rs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index ca0b2967b..28d40afaf 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -28,6 +28,7 @@ pub trait FCircuit { + AllocVar + AbsorbableGadget; type ExternalInputs; + type ExternalOutputs; fn dummy_state(&self) -> Self::State; @@ -42,7 +43,7 @@ pub trait FCircuit { i: FpVar, z_i: Self::StateVar, external_inputs: Self::ExternalInputs, // inputs that are not part of the state - ) -> Result; + ) -> Result<(Self::StateVar, Self::ExternalOutputs), SynthesisError>; } #[derive(Clone, Debug, PartialEq)] diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs index dccbe34a1..463bbaa65 100644 --- a/crates/primitives/src/circuits/utils.rs +++ b/crates/primitives/src/circuits/utils.rs @@ -53,6 +53,7 @@ impl FCircuit for CircuitForTest { type StateVar = [FpVar; 1]; type ExternalInputs = (); + type ExternalOutputs = (); fn dummy_state(&self) -> Self::State { [F::zero(); 1] @@ -66,7 +67,7 @@ impl FCircuit for CircuitForTest { _i: FpVar, z_i: Self::StateVar, _external_inputs: Self::ExternalInputs, - ) -> Result { + ) -> Result<(Self::StateVar, Self::ExternalOutputs), SynthesisError> { // Variable 0 (implicitly added by arkworks as 1) // Variable 1 let x = if let FpVar::Var(x) = z_i[0].clone() { @@ -99,7 +100,7 @@ impl FCircuit for CircuitForTest { || Variable::one().into(), || y.variable.into(), )?; - Ok([FpVar::Var(x_cube_plus_x_plus_5)]) + Ok(([FpVar::Var(x_cube_plus_x_plus_5)], ())) } } From 3f1ed979d8d11de982ec29384e2c3714d7b5135f Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 25 Nov 2025 00:52:37 +0800 Subject: [PATCH 15/93] Refactor code for in-circuit relation checks --- .../primitives/src/algebra/field/emulated.rs | 65 ++++++++++++++++++- crates/primitives/src/algebra/field/mod.rs | 30 ++++++++- crates/primitives/src/algebra/mod.rs | 3 - crates/primitives/src/algebra/ops/eq.rs | 18 ++--- crates/primitives/src/arithmetizations/mod.rs | 19 +++--- .../src/arithmetizations/r1cs/circuits.rs | 31 ++++----- crates/primitives/src/commitments/mod.rs | 33 ++-------- crates/primitives/src/commitments/pedersen.rs | 13 +--- crates/primitives/src/relations/mod.rs | 8 ++- .../src/transcripts/griffin/sponge.rs | 2 +- 10 files changed, 141 insertions(+), 81 deletions(-) diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs index 015140d01..fcfb45b76 100644 --- a/crates/primitives/src/algebra/field/emulated.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -22,9 +22,10 @@ use num_traits::Signed; use crate::{ algebra::{ - field::SonobeField, + field::{SonobeField, TwoStageFieldVar}, ops::{ bits::{FromBitsGadget, ToBitsGadgetExt}, + eq::EquivalenceGadget, matrix::{MatrixGadget, SparseMatrixVar}, vector::VectorGadget, }, @@ -565,6 +566,62 @@ impl } } +impl + EquivalenceGadget> for IntVarInner +{ + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_equal(other) + } +} + +impl + EquivalenceGadget> for IntVarInner +{ + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_congruent(other) + } +} + +impl + EquivalenceGadget> for IntVarInner +{ + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_congruent(other) + } +} + +impl + EquivalenceGadget> for IntVarInner +{ + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_congruent(other) + } +} + +impl EquivalenceGadget> for IntVarInner { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_equal(other) + } +} + +impl EquivalenceGadget> for IntVarInner { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_equal_unaligned(other) + } +} + +impl EquivalenceGadget> for IntVarInner { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_equal_unaligned(other) + } +} + +impl EquivalenceGadget> for IntVarInner { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_equal_unaligned(other) + } +} + impl TryFrom> for IntVarInner { @@ -575,6 +632,10 @@ impl TryFrom TwoStageFieldVar for IntVarInner { + type Intermediate = IntVarInner; +} + impl EqGadget for IntVarInner { fn is_eq(&self, other: &Self) -> Result, SynthesisError> { let mut result = Boolean::TRUE; @@ -858,7 +919,7 @@ impl AllocVar<(BigInt, Bound), F> for IntVarInner>()?; - let bounds = compute_bounds(&lb, &ub, F::BITS_PER_LIMB); + let bounds = compute_bounds(lb, ub, F::BITS_PER_LIMB); let var = Self::new(limbs, bounds); diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs index 5caac15b2..1037c875f 100644 --- a/crates/primitives/src/algebra/field/mod.rs +++ b/crates/primitives/src/algebra/field/mod.rs @@ -1,7 +1,11 @@ use ark_ff::{BigInteger, Fp, FpConfig, PrimeField}; use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; use ark_relations::gr1cs::SynthesisError; -use ark_std::{any::TypeId, mem::transmute_copy}; +use ark_std::{ + any::TypeId, + mem::transmute_copy, + ops::{Add, Mul}, +}; use crate::{ algebra::{field::emulated::EmulatedFieldVar, Val}, @@ -100,3 +104,27 @@ impl InputizeEmulated for P { .collect() } } + +pub trait TwoStageFieldVar: + Clone + + Add + + for<'a> Add<&'a Self, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self, Output = Self::Intermediate> +{ + type Intermediate: Clone + + From + + TryInto + + Add + + for<'a> Add<&'a Self::Intermediate, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self::Intermediate, Output = Self::Intermediate> + + Add + + for<'a> Add<&'a Self, Output = Self::Intermediate> + + Mul + + for<'a> Mul<&'a Self, Output = Self::Intermediate>; +} + +impl TwoStageFieldVar for FpVar { + type Intermediate = Self; +} diff --git a/crates/primitives/src/algebra/mod.rs b/crates/primitives/src/algebra/mod.rs index 0a9d623dd..91f50165f 100644 --- a/crates/primitives/src/algebra/mod.rs +++ b/crates/primitives/src/algebra/mod.rs @@ -13,6 +13,3 @@ pub trait Val { type EmulatedVar: AllocVar + GR1CSVar; } - -pub type Var = ::Var; -pub type EmulatedVar = ::EmulatedVar; \ No newline at end of file diff --git a/crates/primitives/src/algebra/ops/eq.rs b/crates/primitives/src/algebra/ops/eq.rs index a63373fa1..67be1e997 100644 --- a/crates/primitives/src/algebra/ops/eq.rs +++ b/crates/primitives/src/algebra/ops/eq.rs @@ -2,20 +2,22 @@ use ark_ff::PrimeField; use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; -/// `EquivalenceGadget` enforces that two in-circuit variables are equivalent, -/// where the equivalence relation is parameterized by `M`: -/// - For `FpVar`, it is simply an equality relation, and `M` is unused. -/// - For `NonNativeUintVar`, we consider equivalence as a congruence relation, -/// in terms of modular arithmetic, so `M` specifies the modulus. -pub trait EquivalenceGadget { +/// `EquivalenceGadget` enforces that two in-circuit variables are "equivalent". +/// +/// This does not only allow us to ensure the equality of two variables of the +/// same type, but can also be used for guaranteeing variables of different +/// types represent the "same" (depending on the context) value. +pub trait EquivalenceGadget { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError>; } -impl EquivalenceGadget for FpVar { + +impl EquivalenceGadget> for FpVar { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { self.enforce_equal(other) } } -impl> EquivalenceGadget for [T] { + +impl> EquivalenceGadget<[T]> for [T] { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { self.iter() .zip(other) diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index f5f391209..a9dd944ae 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -2,7 +2,7 @@ use ark_relations::gr1cs::SynthesisError; use ark_std::{fmt::Debug, log2}; use thiserror::Error; -use crate::relations::Relation; +use crate::relations::{Relation, RelationGadget}; pub mod ccs; pub mod r1cs; @@ -183,16 +183,15 @@ pub trait ArithRelationGadget { /// Returns the evaluation result. fn eval_relation(&self, w: &WVar, u: &UVar) -> Result; - /// Generates constraints for enforcing that witness `w` and instance `u` - /// satisfy the constraint system `self` by first computing the evaluation - /// result and then checking the validity of the evaluation result. - fn enforce_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError> { - let e = self.eval_relation(w, u)?; - Self::enforce_evaluation(w, u, e) - } - /// Generates constraints for enforcing that the evaluation result is valid. /// The witness `w` and instance `u` are also parameters, because the /// validity check may need information contained in `w` and/or `u`. - fn enforce_evaluation(w: &WVar, u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError>; + fn check_evaluation(w: &WVar, u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError>; +} + +impl> RelationGadget for A { + fn check_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError> { + let e = self.eval_relation(w, u)?; + Self::check_evaluation(w, u, e) + } } diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index 9469f2407..e45f19853 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -1,7 +1,7 @@ use ark_ff::PrimeField; use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, marker::PhantomData, One}; +use ark_std::{borrow::Borrow, One}; use super::R1CS; use crate::{ @@ -15,40 +15,37 @@ use crate::{ }; /// An in-circuit representation of the `R1CS` struct. -/// -/// `M` is for the modulo operation involved in the satisfiability check when -/// the underlying `FVar` is `NonNativeUintVar`. #[allow(non_snake_case)] #[derive(Debug, Clone)] -pub struct R1CSMatricesVar { - _m: PhantomData, +pub struct R1CSMatricesVar { pub A: SparseMatrixVar, pub B: SparseMatrixVar, pub C: SparseMatrixVar, } impl> - AllocVar, ConstraintF> for R1CSMatricesVar + AllocVar, ConstraintF> for R1CSMatricesVar { fn new_variable>>( cs: impl Into>, f: impl FnOnce() -> Result, - _mode: AllocationMode, + mode: AllocationMode, ) -> Result { f().and_then(|val| { let cs = cs.into(); + let val = val.borrow(); + Ok(Self { - _m: PhantomData, - A: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().A)?, - B: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().B)?, - C: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().C)?, + A: SparseMatrixVar::::new_variable(cs.clone(), || Ok(&val.A), mode)?, + B: SparseMatrixVar::::new_variable(cs.clone(), || Ok(&val.B), mode)?, + C: SparseMatrixVar::::new_variable(cs.clone(), || Ok(&val.C), mode)?, }) }) } } -impl R1CSMatricesVar +impl R1CSMatricesVar where SparseMatrixVar: MatrixGadget, [FVar]: VectorGadget, @@ -69,11 +66,11 @@ where } } -impl, UVar: AsRef<[FVar]>> ArithRelationGadget - for R1CSMatricesVar +impl, UVar: AsRef<[FVar]>> ArithRelationGadget + for R1CSMatricesVar where SparseMatrixVar: MatrixGadget, - [FVar]: VectorGadget + EquivalenceGadget, + [FVar]: VectorGadget + EquivalenceGadget, FVar: Clone + One, { /// Evaluation is a tuple of two vectors (`AzBz` and `uCz`) instead of a @@ -85,7 +82,7 @@ where self.eval_assignments((FVar::one(), u.as_ref(), w.as_ref()).into()) } - fn enforce_evaluation( + fn check_evaluation( _w: &WVar, _u: &UVar, (lhs, rhs): Self::Evaluation, diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index 816c0b4be..a78023d70 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,6 +1,6 @@ use ark_ff::UniformRand; use ark_r1cs_std::{ - alloc::AllocVar, eq::EqGadget, fields::fp::FpVar, select::CondSelectGadget, GR1CSVar, + alloc::AllocVar, fields::fp::FpVar, select::CondSelectGadget, GR1CSVar, }; use ark_relations::gr1cs::SynthesisError; use ark_std::{ @@ -13,12 +13,10 @@ use thiserror::Error; use crate::{ algebra::{ - field::emulated::{EmulatedFieldVar, IntVarInner}, - group::emulated::EmulatedAffineVar, - ops::bits::FromBitsGadget, - Var, + Val, field::{TwoStageFieldVar, emulated::{EmulatedFieldVar, IntVarInner}}, group::emulated::EmulatedAffineVar, ops::bits::FromBitsGadget + // Var, }, - traits::{SonobeCurve, SonobeField, CF1, CF2}, + traits::{CF1, CF2, SonobeCurve, SonobeField}, transcripts::{Absorbable, AbsorbableGadget}, }; @@ -85,27 +83,12 @@ pub trait VectorCommitmentGadgetDef: Clone { type ConstraintField: SonobeField; type KeyVar; - type ScalarVar: Clone - + EqGadget - + AbsorbableGadget + type ScalarVar: AbsorbableGadget + CondSelectGadget + FromBitsGadget + AllocVar<::Scalar, Self::ConstraintField> + GR1CSVar::Scalar> - + Add - + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> - + Mul - + for<'a> Mul<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar>; - type IntermediateScalarVar: Clone - + TryInto - + Add - + for<'a> Add<&'a Self::IntermediateScalarVar, Output = Self::IntermediateScalarVar> - + Mul - + for<'a> Mul<&'a Self::IntermediateScalarVar, Output = Self::IntermediateScalarVar> - + Add - + for<'a> Add<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar> - + Mul - + for<'a> Mul<&'a Self::ScalarVar, Output = Self::IntermediateScalarVar>; + + TwoStageFieldVar; type CommitmentVar: Clone + AbsorbableGadget + CondSelectGadget @@ -136,14 +119,12 @@ pub trait GroupBasedVectorCommitment: + VectorCommitmentGadgetDef< ConstraintField = CF2, ScalarVar = EmulatedFieldVar, Self::Scalar>, - IntermediateScalarVar = IntVarInner, Self::Scalar, false>, - CommitmentVar = Var, + CommitmentVar = ::Var, Native = Self, >; type Gadget2: VectorCommitmentGadgetDef< ConstraintField = Self::Scalar, ScalarVar = FpVar, - IntermediateScalarVar = FpVar, CommitmentVar = EmulatedAffineVar, Native = Self, >; diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 5ec639358..0eb825bb3 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -8,10 +8,7 @@ use super::{ CommitmentKey, Error, VectorCommitmentDef, VectorCommitmentGadgetDef, VectorCommitmentOps, }; use crate::{ - algebra::{ - field::emulated::{EmulatedFieldVar, IntVarInner}, - group::emulated::EmulatedAffineVar, - }, + algebra::{field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar}, commitments::{GroupBasedVectorCommitment, VectorCommitmentGadgetOps}, traits::{SonobeCurve, CF1, CF2}, utils::null::Null, @@ -98,8 +95,6 @@ impl VectorCommitmentGadgetDef for PedersenGadget { type ScalarVar = EmulatedFieldVar, CF1>; - type IntermediateScalarVar = IntVarInner, CF1, false>; - type CommitmentVar = C::Var; type RandomnessVar = Null; @@ -114,8 +109,6 @@ impl VectorCommitmentGadgetDef for PedersenGadget { type ScalarVar = EmulatedFieldVar, CF1>; - type IntermediateScalarVar = IntVarInner, CF1, false>; - type CommitmentVar = C::Var; type RandomnessVar = EmulatedFieldVar, CF1>; @@ -130,8 +123,6 @@ impl VectorCommitmentGadgetDef for PedersenEmulatedGadget>; - type IntermediateScalarVar = FpVar>; - type CommitmentVar = EmulatedAffineVar, C>; type RandomnessVar = Null; @@ -149,8 +140,6 @@ impl VectorCommitmentGadgetDef for PedersenEmulatedGadget>; - type IntermediateScalarVar = FpVar>; - type CommitmentVar = EmulatedAffineVar, C>; type RandomnessVar = FpVar>; diff --git a/crates/primitives/src/relations/mod.rs b/crates/primitives/src/relations/mod.rs index 0e3467057..529af6565 100644 --- a/crates/primitives/src/relations/mod.rs +++ b/crates/primitives/src/relations/mod.rs @@ -1,3 +1,4 @@ +use ark_relations::gr1cs::SynthesisError; use ark_std::{error::Error, rand::RngCore}; pub trait Relation { @@ -7,11 +8,16 @@ pub trait Relation { fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error>; } +pub trait RelationGadget { + /// Checks if witness `w` and instance `u` satisfy the relation `self` + fn check_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError>; +} + /// `WitnessInstanceSampler` allows sampling a random witness-instance pair that /// satisfies the relation `self`. pub trait WitnessInstanceSampler { type Source; - type Error: Error + 'static; + type Error: Error; fn sample(&self, source: Self::Source, rng: impl RngCore) -> Result<(W, U), Self::Error>; } diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs index 28d89efa7..fc732eadf 100644 --- a/crates/primitives/src/transcripts/griffin/sponge.rs +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -352,7 +352,7 @@ impl TranscriptVar for GriffinSpongeVar { pub mod tests { use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; use ark_ec::PrimeGroup; - use ark_ff::{BigInteger, PrimeField, UniformRand}; + use ark_ff::UniformRand; use ark_r1cs_std::{ alloc::AllocVar, fields::fp::FpVar, From f7a73b3b2e8ac3d789aae705abbdcab29ca3cc8b Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 16/93] Adjust the naming of traits for gadgets --- crates/primitives/src/commitments/mod.rs | 22 +++++++++---------- crates/primitives/src/commitments/pedersen.rs | 16 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index a78023d70..b9a948ab5 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,7 +1,5 @@ use ark_ff::UniformRand; -use ark_r1cs_std::{ - alloc::AllocVar, fields::fp::FpVar, select::CondSelectGadget, GR1CSVar, -}; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, select::CondSelectGadget, GR1CSVar}; use ark_relations::gr1cs::SynthesisError; use ark_std::{ fmt::Debug, @@ -13,10 +11,12 @@ use thiserror::Error; use crate::{ algebra::{ - Val, field::{TwoStageFieldVar, emulated::{EmulatedFieldVar, IntVarInner}}, group::emulated::EmulatedAffineVar, ops::bits::FromBitsGadget - // Var, + field::{emulated::EmulatedFieldVar, TwoStageFieldVar}, + group::emulated::EmulatedAffineVar, + ops::bits::FromBitsGadget, + Val, }, - traits::{CF1, CF2, SonobeCurve, SonobeField}, + traits::{SonobeCurve, SonobeField, CF1, CF2}, transcripts::{Absorbable, AbsorbableGadget}, }; @@ -79,7 +79,7 @@ pub trait VectorCommitmentOps: VectorCommitmentDef { ) -> Result<(), Error>; } -pub trait VectorCommitmentGadgetDef: Clone { +pub trait VectorCommitmentDefGadget: Clone { type ConstraintField: SonobeField; type KeyVar; @@ -100,7 +100,7 @@ pub trait VectorCommitmentGadgetDef: Clone { type Native: VectorCommitmentDef; } -pub trait VectorCommitmentGadgetOps: VectorCommitmentGadgetDef { +pub trait VectorCommitmentOpsGadget: VectorCommitmentDefGadget { fn open( ck: &Self::KeyVar, v: &[Self::ScalarVar], @@ -115,14 +115,14 @@ pub trait GroupBasedVectorCommitment: Scalar = CF1<::Commitment>, > + VectorCommitmentOps { - type Gadget1: VectorCommitmentGadgetOps - + VectorCommitmentGadgetDef< + type Gadget1: VectorCommitmentOpsGadget + + VectorCommitmentDefGadget< ConstraintField = CF2, ScalarVar = EmulatedFieldVar, Self::Scalar>, CommitmentVar = ::Var, Native = Self, >; - type Gadget2: VectorCommitmentGadgetDef< + type Gadget2: VectorCommitmentDefGadget< ConstraintField = Self::Scalar, ScalarVar = FpVar, CommitmentVar = EmulatedAffineVar, diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 0eb825bb3..7d470b933 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -5,11 +5,11 @@ use ark_relations::gr1cs::SynthesisError; use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; use super::{ - CommitmentKey, Error, VectorCommitmentDef, VectorCommitmentGadgetDef, VectorCommitmentOps, + CommitmentKey, Error, VectorCommitmentDef, VectorCommitmentDefGadget, VectorCommitmentOps, }; use crate::{ algebra::{field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar}, - commitments::{GroupBasedVectorCommitment, VectorCommitmentGadgetOps}, + commitments::{GroupBasedVectorCommitment, VectorCommitmentOpsGadget}, traits::{SonobeCurve, CF1, CF2}, utils::null::Null, }; @@ -88,7 +88,7 @@ impl VectorCommitmentDef for Pedersen { type Randomness = C::ScalarField; } -impl VectorCommitmentGadgetDef for PedersenGadget { +impl VectorCommitmentDefGadget for PedersenGadget { type ConstraintField = CF2; type KeyVar = Vec; @@ -102,7 +102,7 @@ impl VectorCommitmentGadgetDef for PedersenGadget { type Native = Pedersen; } -impl VectorCommitmentGadgetDef for PedersenGadget { +impl VectorCommitmentDefGadget for PedersenGadget { type ConstraintField = CF2; type KeyVar = (Vec, C::Var); @@ -116,7 +116,7 @@ impl VectorCommitmentGadgetDef for PedersenGadget { type Native = Pedersen; } -impl VectorCommitmentGadgetDef for PedersenEmulatedGadget { +impl VectorCommitmentDefGadget for PedersenEmulatedGadget { type ConstraintField = CF1; type KeyVar = Vec, C>>; @@ -130,7 +130,7 @@ impl VectorCommitmentGadgetDef for PedersenEmulatedGadget; } -impl VectorCommitmentGadgetDef for PedersenEmulatedGadget { +impl VectorCommitmentDefGadget for PedersenEmulatedGadget { type ConstraintField = CF1; type KeyVar = ( @@ -313,7 +313,7 @@ impl PedersenGadget { } } -impl VectorCommitmentGadgetOps for PedersenGadget { +impl VectorCommitmentOpsGadget for PedersenGadget { fn open( ck: &Vec, v: &[EmulatedFieldVar, CF1>], @@ -330,7 +330,7 @@ impl VectorCommitmentGadgetOps for PedersenGadget { } } -impl VectorCommitmentGadgetOps for PedersenGadget { +impl VectorCommitmentOpsGadget for PedersenGadget { fn open( (g, h): &(Vec, C::Var), v: &[EmulatedFieldVar, CF1>], From 5c9a8c4e223022895b25029b50921bba93d7c13e Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 17/93] Clean up --- crates/primitives/src/algebra/group/mod.rs | 57 +++++------ crates/primitives/src/transcripts/mod.rs | 3 +- crates/primitives/src/utils/mod.rs | 1 - crates/primitives/src/utils/vec.rs | 109 --------------------- 4 files changed, 29 insertions(+), 141 deletions(-) delete mode 100644 crates/primitives/src/utils/vec.rs diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs index a429f8f2e..5ab14a05d 100644 --- a/crates/primitives/src/algebra/group/mod.rs +++ b/crates/primitives/src/algebra/group/mod.rs @@ -9,9 +9,6 @@ use ark_r1cs_std::{ groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, }; use ark_relations::gr1cs::SynthesisError; -use ark_std::mem::swap; -use num_bigint::BigInt; -use num_integer::Integer; use crate::{ algebra::{field::SonobeField, group::emulated::EmulatedAffineVar, Val}, @@ -107,34 +104,34 @@ impl> } } -fn lattice_reduction_2x2( - mut b1: (BigInt, BigInt), - mut b2: (BigInt, BigInt), -) -> ((BigInt, BigInt), (BigInt, BigInt)) { - loop { - let mut b1_norm_sq = &b1.0 * &b1.0 + &b1.1 * &b1.1; - let mut b2_norm_sq = &b2.0 * &b2.0 + &b2.1 * &b2.1; - - if b1_norm_sq > b2_norm_sq { - swap(&mut b1, &mut b2); - swap(&mut b1_norm_sq, &mut b2_norm_sq); - } - - let (mut m, r) = (&b1.0 * &b2.0 + &b1.1 * &b2.1).div_rem(&b1_norm_sq); - if &r + &r >= b1_norm_sq { - m += BigInt::one(); - } - - if m.is_zero() { - break; - } - - b2.0 -= &m * &b1.0; - b2.1 -= &m * &b1.1; - } +// fn lattice_reduction_2x2( +// mut b1: (BigInt, BigInt), +// mut b2: (BigInt, BigInt), +// ) -> ((BigInt, BigInt), (BigInt, BigInt)) { +// loop { +// let mut b1_norm_sq = &b1.0 * &b1.0 + &b1.1 * &b1.1; +// let mut b2_norm_sq = &b2.0 * &b2.0 + &b2.1 * &b2.1; + +// if b1_norm_sq > b2_norm_sq { +// swap(&mut b1, &mut b2); +// swap(&mut b1_norm_sq, &mut b2_norm_sq); +// } + +// let (mut m, r) = (&b1.0 * &b2.0 + &b1.1 * &b2.1).div_rem(&b1_norm_sq); +// if &r + &r >= b1_norm_sq { +// m += BigInt::one(); +// } + +// if m.is_zero() { +// break; +// } + +// b2.0 -= &m * &b1.0; +// b2.1 -= &m * &b1.1; +// } - (b1, b2) -} +// (b1, b2) +// } // impl PointScalarMulGadget> for C { // fn mul_scalar(&self, scalar: &impl ToBitsGadget>) -> Result { diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index 7f5ebc8e3..4fdaffa78 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -1,8 +1,9 @@ -pub use absorbable::{Absorbable, AbsorbableGadget}; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; +pub use self::absorbable::{Absorbable, AbsorbableGadget}; + pub mod absorbable; pub mod griffin; pub mod poseidon; diff --git a/crates/primitives/src/utils/mod.rs b/crates/primitives/src/utils/mod.rs index 1e8ac48a7..2cf111496 100644 --- a/crates/primitives/src/utils/mod.rs +++ b/crates/primitives/src/utils/mod.rs @@ -1,2 +1 @@ pub mod null; -// pub mod vec; diff --git a/crates/primitives/src/utils/vec.rs b/crates/primitives/src/utils/vec.rs deleted file mode 100644 index f1c9fec21..000000000 --- a/crates/primitives/src/utils/vec.rs +++ /dev/null @@ -1,109 +0,0 @@ -use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - fields::fp::FpVar, - prelude::Boolean, - select::CondSelectGadget, - GR1CSVar, -}; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{ - borrow::Borrow, - fmt::Debug, - ops::{Deref, DerefMut}, -}; - -use crate::{ - arithmetizations::ArithConfig, - circuits::var::Var, - traits::Dummy, - transcripts::{Absorbable, AbsorbableGadget}, -}; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct WrappedVec(Vec); - -impl Deref for WrappedVec { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for WrappedVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From> for WrappedVec { - fn from(v: Vec) -> Self { - Self(v) - } -} - -impl Absorbable for WrappedVec { - fn absorb_into(&self, dest: &mut Vec) { - self.0.absorb_into(dest) - } -} - -impl> AbsorbableGadget for WrappedVec { - fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { - self.0.absorb_into(dest) - } -} - -impl, Y, F: Field> AllocVar, F> for WrappedVec { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let v = f()?; - Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(|v| Self(v)) - } -} - -impl> CondSelectGadget for WrappedVec { - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - if true_value.len() != false_value.len() { - return Err(SynthesisError::Unsatisfiable); - } - Ok(WrappedVec( - true_value - .0 - .iter() - .zip(false_value.0.iter()) - .map(|(t, f)| cond.select(t, f)) - .collect::>()?, - )) - } -} - -impl> GR1CSVar for WrappedVec { - type Value = WrappedVec; - - fn cs(&self) -> ConstraintSystemRef { - self.0.cs() - } - - fn value(&self) -> Result { - self.0.value().map(WrappedVec) - } -} - -impl> Var for WrappedVec { - type Native = WrappedVec; -} - -impl Dummy<&A> for WrappedVec { - fn dummy(cfg: &A) -> Self { - vec![V::default(); cfg.n_public_inputs()].into() - } -} From a4c50000fe9e621ab869d2d26fec2b748bce1467 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 18/93] Discussion about `Absorbable`'s design --- .../primitives/src/transcripts/absorbable.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/primitives/src/transcripts/absorbable.rs b/crates/primitives/src/transcripts/absorbable.rs index 8143a76d1..ba7de434c 100644 --- a/crates/primitives/src/transcripts/absorbable.rs +++ b/crates/primitives/src/transcripts/absorbable.rs @@ -2,6 +2,39 @@ use ark_ff::PrimeField; use ark_r1cs_std::fields::fp::FpVar; use ark_relations::gr1cs::SynthesisError; +// TODO (@winderica): +// +// Ideally this trait should be defined as follows, so that we can use it for +// absorbing values into bits/bytes/etc., in addition to field elements. +// (Although Arkworks' `Absorb` trait covers both bytes and field elements, it +// requires downstream types to support absorbing into both as well, even if the +// downstream type doesn't support/is unrelated to one absorbing target.) +// +// ```rs +// pub trait Absorbable { +// fn absorb_into(&self, dest: &mut Vec); + +// fn to_absorbable(&self) -> Vec { +// let mut result = Vec::new(); +// self.absorb_into(&mut result); +// result +// } +// } +// ``` +// +// But my attempt was unsuccessful. In our use case, `SonobeField` needs to be +// absorbed into prime fields that are unknown when making the definition. Due +// to the `F` type parameter in `Absorbable`, I have three options: +// 1. Define `SonobeField` as `SonobeField: Absorbable`. This means that +// I need to add `F` to everywhere `SonobeField` is used, making the codebase +// much more verbose. +// 2. Remove the `Absorbable` bound from `SonobeField`, but instead manually add +// `Absorbable` to `T: SonobeField`'s bounds whenever we need `T` to be +// absorbable. This also increases the verbosity a lot. +// 3. Wait for https://github.com/rust-lang/rust/issues/108185 to be resolved, +// so I can define `SonobeField: for Absorbable`. +// Personally I think the best option is 3. File an issue or submit a PR if you +// have better solution :) pub trait Absorbable { fn absorb_into(&self, dest: &mut Vec); From a36bf463818ea7c00bd67545188431c79ea900a6 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 19/93] Specify MSRV --- Cargo.toml | 3 ++- crates/primitives/src/transcripts/griffin/mod.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b56b4da35..2c8772be8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,9 +27,10 @@ ark-crypto-primitives = { git = "https://github.com/winderica/crypto-primitives" ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" } # "sw-fix-updated" branch [workspace.package] -edition = "2021" +edition = "2024" license = "MIT" repository = "https://github.com/privacy-scaling-explorations/sonobe/" +rust-version = "1.85.1" [workspace.dependencies] acvm = { git = "https://github.com/winderica/noir", rev = "fc9e99", default-features = false } # "arkworks-next" branch diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs index b4c931d92..1b7134234 100644 --- a/crates/primitives/src/transcripts/griffin/mod.rs +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -50,7 +50,7 @@ impl GriffinParams { pub const INIT_SHAKE: &'static str = "Griffin"; pub fn new(t: usize, d: usize, rounds: usize) -> Self { - assert!(t == 3 || t.is_multiple_of(4)); + assert!(t == 3 || t % 4 == 0); assert!(d == 3 || d == 5); assert!(rounds >= 1); From ebae8c4f016cd296be077b701da6b3d06fd57d12 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 15:50:26 +0800 Subject: [PATCH 20/93] Improve naming and API design --- .../primitives/src/algebra/field/emulated.rs | 22 ++-- crates/primitives/src/algebra/field/mod.rs | 6 +- .../primitives/src/algebra/group/emulated.rs | 16 ++- crates/primitives/src/algebra/group/mod.rs | 10 +- crates/primitives/src/algebra/mod.rs | 2 +- crates/primitives/src/algebra/ops/bits.rs | 2 +- crates/primitives/src/algebra/ops/matrix.rs | 4 +- crates/primitives/src/algebra/ops/poly.rs | 4 +- crates/primitives/src/algebra/ops/pow.rs | 2 +- .../src/arithmetizations/ccs/circuits.rs | 2 +- .../src/arithmetizations/ccs/mod.rs | 20 +-- .../src/arithmetizations/r1cs/circuits.rs | 2 +- .../src/arithmetizations/r1cs/mod.rs | 47 ++++--- crates/primitives/src/circuits/mod.rs | 115 +++++++++--------- crates/primitives/src/circuits/utils.rs | 3 +- crates/primitives/src/commitments/mod.rs | 68 +++++------ crates/primitives/src/commitments/pedersen.rs | 34 +++--- crates/primitives/src/sumcheck/circuits.rs | 12 +- crates/primitives/src/sumcheck/mod.rs | 8 +- crates/primitives/src/sumcheck/utils.rs | 8 +- crates/primitives/src/traits.rs | 2 +- .../primitives/src/transcripts/griffin/mod.rs | 6 +- .../src/transcripts/griffin/sponge.rs | 10 +- .../src/transcripts/poseidon/sponge.rs | 12 +- crates/primitives/src/utils/null.rs | 2 +- 25 files changed, 216 insertions(+), 203 deletions(-) diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs index fcfb45b76..a4c11e64f 100644 --- a/crates/primitives/src/algebra/field/emulated.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -1,12 +1,12 @@ use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_r1cs_std::{ + GR1CSVar, alloc::{AllocVar, AllocationMode}, boolean::Boolean, convert::ToBitsGadget, - fields::{fp::FpVar, FieldVar}, + fields::{FieldVar, fp::FpVar}, prelude::EqGadget, select::CondSelectGadget, - GR1CSVar, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{ @@ -566,32 +566,32 @@ impl } } -impl - EquivalenceGadget> for IntVarInner +impl EquivalenceGadget> + for IntVarInner { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { self.enforce_equal(other) } } -impl - EquivalenceGadget> for IntVarInner +impl EquivalenceGadget> + for IntVarInner { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { self.enforce_congruent(other) } } -impl - EquivalenceGadget> for IntVarInner +impl EquivalenceGadget> + for IntVarInner { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { self.enforce_congruent(other) } } -impl - EquivalenceGadget> for IntVarInner +impl EquivalenceGadget> + for IntVarInner { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { self.enforce_congruent(other) @@ -1147,7 +1147,7 @@ mod tests { use ark_ff::Field; use ark_pallas::{Fq, Fr}; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{error::Error, test_rng, UniformRand}; + use ark_std::{UniformRand, error::Error, test_rng}; use num_bigint::RandBigInt; use super::*; diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs index 1037c875f..64b1316b9 100644 --- a/crates/primitives/src/algebra/field/mod.rs +++ b/crates/primitives/src/algebra/field/mod.rs @@ -1,5 +1,5 @@ use ark_ff::{BigInteger, Fp, FpConfig, PrimeField}; -use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; use ark_relations::gr1cs::SynthesisError; use ark_std::{ any::TypeId, @@ -8,14 +8,14 @@ use ark_std::{ }; use crate::{ - algebra::{field::emulated::EmulatedFieldVar, Val}, + algebra::{Val, field::emulated::EmulatedFieldVar}, traits::{Inputize, InputizeEmulated}, transcripts::{Absorbable, AbsorbableGadget}, }; pub mod emulated; -/// `Field` trait is a wrapper around `PrimeField` that also includes the +/// `SonobeField` trait is a wrapper around `PrimeField` that also includes the /// necessary bounds for the field to be used conveniently in folding schemes. pub trait SonobeField: PrimeField diff --git a/crates/primitives/src/algebra/group/emulated.rs b/crates/primitives/src/algebra/group/emulated.rs index 4ebd0cf79..42b93e3e4 100644 --- a/crates/primitives/src/algebra/group/emulated.rs +++ b/crates/primitives/src/algebra/group/emulated.rs @@ -1,12 +1,12 @@ -use ark_ec::{short_weierstrass::SWFlags, AffineRepr}; +use ark_ec::{AffineRepr, short_weierstrass::SWFlags}; use ark_ff::Zero; use ark_r1cs_std::{ + GR1CSVar, alloc::{AllocVar, AllocationMode}, eq::EqGadget, fields::fp::FpVar, prelude::Boolean, select::CondSelectGadget, - GR1CSVar, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; @@ -18,9 +18,13 @@ use crate::{ transcripts::AbsorbableGadget, }; -/// NonNativeAffineVar represents an elliptic curve point in Affine representation in the non-native -/// field, over the constraint field. It is not intended to perform operations, but just to contain -/// the affine coordinates in order to perform hash operations of the point. +/// `EmulatedAffineVar` defines an in-circuit elliptic curve point in its affine +/// representation, where the coordinates are non-native field variables in the +/// curve's base field `Target::BaseField`, emulated over the constraint field +/// `Base`. +/// +/// It is not intended to perform operations, but just to record the coordinates +/// in order to perform hash operations of the point. #[derive(Debug, Clone)] pub struct EmulatedAffineVar { pub x: EmulatedFieldVar, @@ -134,7 +138,7 @@ mod tests { use ark_pallas::{Fq, Fr, PallasConfig, Projective}; use ark_r1cs_std::groups::curves::short_weierstrass::ProjectiveVar; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{error::Error, UniformRand}; + use ark_std::{UniformRand, error::Error}; use super::*; use crate::{ diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs index 5ab14a05d..3abf989aa 100644 --- a/crates/primitives/src/algebra/group/mod.rs +++ b/crates/primitives/src/algebra/group/mod.rs @@ -1,17 +1,17 @@ use ark_ec::{ - short_weierstrass::{Projective, SWCurveConfig}, AffineRepr, CurveGroup, PrimeGroup, + short_weierstrass::{Projective, SWCurveConfig}, }; use ark_ff::{Field, One, PrimeField, Zero}; use ark_r1cs_std::{ convert::ToConstraintFieldGadget, fields::fp::FpVar, - groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, + groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, }; use ark_relations::gr1cs::SynthesisError; use crate::{ - algebra::{field::SonobeField, group::emulated::EmulatedAffineVar, Val}, + algebra::{Val, field::SonobeField, group::emulated::EmulatedAffineVar}, traits::{Dummy, Inputize, InputizeEmulated}, transcripts::{Absorbable, AbsorbableGadget}, }; @@ -21,7 +21,7 @@ pub mod emulated; pub type CF1 = ::ScalarField; pub type CF2 = <::BaseField as Field>::BasePrimeField; -/// `Curve` trait is a wrapper around `CurveGroup` that also includes the +/// `SonobeCurve` trait is a wrapper around `CurveGroup` that also includes the /// necessary bounds for the curve to be used conveniently in folding schemes. pub trait SonobeCurve: CurveGroup @@ -30,7 +30,7 @@ pub trait SonobeCurve: + InputizeEmulated + Val< Var: CurveVar + AbsorbableGadget, - EmulatedVar = EmulatedAffineVar + EmulatedVar = EmulatedAffineVar, > { } diff --git a/crates/primitives/src/algebra/mod.rs b/crates/primitives/src/algebra/mod.rs index 91f50165f..438d26fca 100644 --- a/crates/primitives/src/algebra/mod.rs +++ b/crates/primitives/src/algebra/mod.rs @@ -1,5 +1,5 @@ use ark_ff::PrimeField; -use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; use crate::traits::SonobeField; diff --git a/crates/primitives/src/algebra/ops/bits.rs b/crates/primitives/src/algebra/ops/bits.rs index 12a0c5dee..e438db2c0 100644 --- a/crates/primitives/src/algebra/ops/bits.rs +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -1,5 +1,5 @@ use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{alloc::AllocVar, boolean::Boolean, eq::EqGadget, fields::fp::FpVar, GR1CSVar}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, eq::EqGadget, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; use crate::algebra::field::emulated::Bound; diff --git a/crates/primitives/src/algebra/ops/matrix.rs b/crates/primitives/src/algebra/ops/matrix.rs index 38ed5d44c..e1347f30e 100644 --- a/crates/primitives/src/algebra/ops/matrix.rs +++ b/crates/primitives/src/algebra/ops/matrix.rs @@ -1,8 +1,8 @@ use ark_ff::PrimeField; use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - fields::{fp::FpVar, FieldVar}, GR1CSVar, + alloc::{AllocVar, AllocationMode}, + fields::{FieldVar, fp::FpVar}, }; use ark_relations::gr1cs::{Matrix, Namespace, SynthesisError}; use ark_std::{borrow::Borrow, ops::Index}; diff --git a/crates/primitives/src/algebra/ops/poly.rs b/crates/primitives/src/algebra/ops/poly.rs index aec303013..91696240a 100644 --- a/crates/primitives/src/algebra/ops/poly.rs +++ b/crates/primitives/src/algebra/ops/poly.rs @@ -1,6 +1,6 @@ use ark_ff::{Field, PrimeField, Zero}; use ark_poly::{DenseMultilinearExtension, EvaluationDomain, GeneralEvaluationDomain}; -use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; use ark_relations::gr1cs::SynthesisError; use ark_std::log2; @@ -25,7 +25,7 @@ pub trait EvaluationDomainGadget { ) -> Result>, SynthesisError>; fn evaluate_vanishing_polynomial_var(&self, tau: &FpVar) - -> Result, SynthesisError>; + -> Result, SynthesisError>; } impl EvaluationDomainGadget for GeneralEvaluationDomain { diff --git a/crates/primitives/src/algebra/ops/pow.rs b/crates/primitives/src/algebra/ops/pow.rs index 918577115..ac78ba6a4 100644 --- a/crates/primitives/src/algebra/ops/pow.rs +++ b/crates/primitives/src/algebra/ops/pow.rs @@ -1,5 +1,5 @@ use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; pub trait Pow: Sized { /// Compute `self^0, self^1, ..., self^{n-1}` diff --git a/crates/primitives/src/arithmetizations/ccs/circuits.rs b/crates/primitives/src/arithmetizations/ccs/circuits.rs index 2abb1271f..f9917cb35 100644 --- a/crates/primitives/src/arithmetizations/ccs/circuits.rs +++ b/crates/primitives/src/arithmetizations/ccs/circuits.rs @@ -6,7 +6,7 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::borrow::Borrow; -use super::{CCSVariant, CCS}; +use super::{CCS, CCSVariant}; use crate::algebra::ops::matrix::SparseMatrixVar; /// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 83424f422..722581134 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -5,10 +5,10 @@ use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, fmt::Debug, marker::Phant #[cfg(feature = "parallel")] use rayon::prelude::*; -use super::{r1cs::R1CS, Arith, ArithRelation, Error}; +use super::{Arith, ArithRelation, Error, r1cs::R1CS}; use crate::{ algebra::ops::poly::MLEHelper, - arithmetizations::{r1cs::R1CSConfig, ArithConfig}, + arithmetizations::{ArithConfig, r1cs::R1CSConfig}, circuits::Assignments, }; @@ -106,14 +106,18 @@ impl CCS { let public_len = z.public.as_ref().len(); let private_len = z.private.as_ref().len(); if public_len != self.n_public_inputs() { - return Err(Error::MalformedAssignments( - format!("The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", self.n_public_inputs(), public_len) - )); + return Err(Error::MalformedAssignments(format!( + "The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", + self.n_public_inputs(), + public_len + ))); } if private_len != self.n_witnesses() { - return Err(Error::MalformedAssignments( - format!("The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", self.n_witnesses(), private_len) - )); + return Err(Error::MalformedAssignments(format!( + "The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", + self.n_witnesses(), + private_len + ))); } // Recall that the evaluation of CCS at z is defined as: diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index e45f19853..54b1e0563 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -1,7 +1,7 @@ use ark_ff::PrimeField; use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, One}; +use ark_std::{One, borrow::Borrow}; use super::R1CS; use crate::{ diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 1def67afc..80d6cf8d3 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -4,9 +4,9 @@ use ark_std::{cfg_into_iter, cfg_iter, iterable::Iterable}; #[cfg(feature = "parallel")] use rayon::prelude::*; -use super::{ccs::CCS, Arith, ArithRelation, Error}; +use super::{Arith, ArithRelation, Error, ccs::CCS}; use crate::{ - arithmetizations::{ccs::CCSVariant, ArithConfig}, + arithmetizations::{ArithConfig, ccs::CCSVariant}, circuits::Assignments, }; @@ -111,14 +111,18 @@ impl R1CS { let public_len = z.public.as_ref().len(); let private_len = z.private.as_ref().len(); if public_len != self.n_public_inputs() { - return Err(Error::MalformedAssignments( - format!("The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", self.n_public_inputs(), public_len) - )); + return Err(Error::MalformedAssignments(format!( + "The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", + self.n_public_inputs(), + public_len + ))); } if private_len != self.n_witnesses() { - return Err(Error::MalformedAssignments( - format!("The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", self.n_witnesses(), private_len) - )); + return Err(Error::MalformedAssignments(format!( + "The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", + self.n_witnesses(), + private_len + ))); } Ok(cfg_iter!(self.A) @@ -249,12 +253,12 @@ pub mod tests { use super::*; use crate::circuits::{ - utils::{constraints_for_test, satisfying_assignments_for_test, CircuitForTest}, - ConstraintSystemExt, + ArithExtractor, AssignmentsExtractor, + utils::{CircuitForTest, constraints_for_test, satisfying_assignments_for_test}, }; #[test] - fn test_constraint_extraction() -> Result<(), Box> { + fn test_satisfiability() -> Result<(), Box> { let mut rng = test_rng(); let circuit = CircuitForTest:: { x: Fr::rand(&mut rng), @@ -262,10 +266,19 @@ pub mod tests { let cs = ConstraintSystem::new_ref(); circuit.generate_constraints(cs.clone())?; assert!(cs.is_satisfied()?); - cs.finalize(); - let cs = cs.into_inner().unwrap(); - assert_eq!(R1CS::from(&cs), constraints_for_test()); + Ok(()) + } + + #[test] + fn test_constraint_extraction() -> Result<(), Box> { + let mut rng = test_rng(); + let circuit = CircuitForTest:: { + x: Fr::rand(&mut rng), + }; + let cs = ArithExtractor::new(); + cs.execute_synthesizer(circuit)?; + assert_eq!(cs.arith::>()?, constraints_for_test()); Ok(()) } @@ -274,11 +287,9 @@ pub mod tests { let mut rng = test_rng(); let x = Fr::rand(&mut rng); let circuit = CircuitForTest:: { x }; - let cs = ConstraintSystem::new_ref(); - circuit.generate_constraints(cs.clone())?; - assert!(cs.is_satisfied()?); - cs.finalize(); + let cs = AssignmentsExtractor::new(); + cs.execute_synthesizer(circuit)?; assert_eq!(cs.assignments()?, satisfying_assignments_for_test(x)); Ok(()) } diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index 28d40afaf..78e5b060d 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -1,12 +1,11 @@ use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, GR1CSVar}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, fields::fp::FpVar}; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, }; use ark_std::{ fmt::Debug, - marker::PhantomData, - ops::{Index, IndexMut}, + ops::{Deref, Index, IndexMut}, }; use crate::transcripts::{Absorbable, AbsorbableGadget}; @@ -95,82 +94,78 @@ impl + AsMut<[F]>> IndexMut for Assignments { } } -pub struct ConstraintSystemBuilder { - _f: PhantomData, - mode: SynthesisMode, - circuit: C, +pub struct ConstraintSystemExt +{ + cs: ConstraintSystemRef, } +impl Deref + for ConstraintSystemExt +{ + type Target = ConstraintSystemRef; -impl ConstraintSystemBuilder<(), ()> { - pub fn new() -> Self { - Self { - _f: PhantomData, - mode: SynthesisMode::Prove { - construct_matrices: true, - generate_lc_assignments: true, - }, - circuit: (), - } + fn deref(&self) -> &Self::Target { + &self.cs } } -impl Default for ConstraintSystemBuilder<(), ()> { - fn default() -> Self { - Self::new() - } -} - -impl ConstraintSystemBuilder { - pub fn with_setup_mode(self) -> Self { - Self { - _f: PhantomData, - mode: SynthesisMode::Setup, - circuit: self.circuit, - } +impl + ConstraintSystemExt +{ + pub fn new() -> Self { + let cs = ConstraintSystem::::new_ref(); + let mode = if ASSIGNMENTS_ENABLED { + SynthesisMode::Prove { + construct_matrices: ARITH_ENABLED, + generate_lc_assignments: ARITH_ENABLED, + } + } else { + SynthesisMode::Setup + }; + cs.set_mode(mode); + Self { cs } } - pub fn with_prove_mode(self) -> Self { - Self { - _f: PhantomData, - mode: SynthesisMode::Prove { - construct_matrices: true, - generate_lc_assignments: true, - }, - circuit: self.circuit, - } + pub fn execute_synthesizer( + &self, + circuit: impl ConstraintSynthesizer, + ) -> Result<(), SynthesisError> { + self.execute_fn(|cs| circuit.generate_constraints(cs)) } - pub fn with_circuit>( - self, - circuit: C, - ) -> ConstraintSystemBuilder { - ConstraintSystemBuilder { - _f: PhantomData, - mode: self.mode, - circuit, + pub fn execute_fn( + &self, + circuit: impl FnOnce(ConstraintSystemRef) -> Result, + ) -> Result { + let result = circuit(self.cs.clone())?; + if ARITH_ENABLED { + self.cs.finalize(); } + Ok(result) } } -impl> ConstraintSystemBuilder { - pub fn synthesize(self) -> Result, SynthesisError> { - let cs = ConstraintSystem::::new_ref(); - cs.set_mode(self.mode); - self.circuit.generate_constraints(cs.clone())?; - cs.finalize(); - Ok(cs.into_inner().unwrap()) +impl Default + for ConstraintSystemExt +{ + fn default() -> Self { + Self::new() } } -pub trait ConstraintSystemExt { - fn assignments(&self) -> Result>, SynthesisError>; +pub type ArithExtractor = ConstraintSystemExt; +pub type AssignmentsExtractor = ConstraintSystemExt; + +impl ArithExtractor { + pub fn arith>>(self) -> Result { + Ok(self.cs.into_inner().unwrap().into()) + } } -impl ConstraintSystemExt for ConstraintSystemRef { - fn assignments(&self) -> Result>, SynthesisError> { - let witness = self.witness_assignment()?.to_vec(); +impl AssignmentsExtractor { + pub fn assignments(self) -> Result>, SynthesisError> { + let witness = self.cs.witness_assignment()?.to_vec(); // skip the first element which is '1' - let instance = self.instance_assignment()?[1..].to_vec(); + let instance = self.cs.instance_assignment()?[1..].to_vec(); Ok((F::one(), instance, witness).into()) } diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs index 463bbaa65..cd345b9d2 100644 --- a/crates/primitives/src/circuits/utils.rs +++ b/crates/primitives/src/circuits/utils.rs @@ -8,7 +8,8 @@ use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, Synthesis use super::Assignments; use crate::{ arithmetizations::r1cs::{R1CS, R1CSConfig}, - circuits::FCircuit, traits::SonobeField, + circuits::FCircuit, + traits::SonobeField, }; pub struct CircuitForTest { diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index b9a948ab5..5e1f2f256 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,5 +1,5 @@ use ark_ff::UniformRand; -use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, select::CondSelectGadget, GR1CSVar}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, fields::fp::FpVar, select::CondSelectGadget}; use ark_relations::gr1cs::SynthesisError; use ark_std::{ fmt::Debug, @@ -11,12 +11,12 @@ use thiserror::Error; use crate::{ algebra::{ - field::{emulated::EmulatedFieldVar, TwoStageFieldVar}, + Val, + field::{TwoStageFieldVar, emulated::EmulatedFieldVar}, group::emulated::EmulatedAffineVar, ops::bits::FromBitsGadget, - Val, }, - traits::{SonobeCurve, SonobeField, CF1, CF2}, + traits::{CF1, CF2, SonobeCurve, SonobeField}, transcripts::{Absorbable, AbsorbableGadget}, }; @@ -26,7 +26,9 @@ pub mod pedersen; #[derive(Debug, Error)] pub enum Error { // Commitment errors - #[error("The message being committed to has length {1}, exceeding the maximum supported length ({0})")] + #[error( + "The message being committed to has length {1}, exceeding the maximum supported length ({0})" + )] MessageTooLong(usize, usize), #[error("Blinding factor not 0 for Commitment without hiding")] BlindingNotZero, @@ -40,7 +42,7 @@ pub trait CommitmentKey: Clone { fn max_scalars_len(&self) -> usize; } -pub trait VectorCommitmentDef: 'static + Clone + Debug + PartialEq + Eq { +pub trait CommitmentDef: 'static + Clone + Debug + PartialEq + Eq { const IS_HIDING: bool; type Key: CommitmentKey; @@ -62,7 +64,7 @@ pub trait VectorCommitmentDef: 'static + Clone + Debug + PartialEq + Eq { + Sum; } -pub trait VectorCommitmentOps: VectorCommitmentDef { +pub trait CommitmentOps: CommitmentDef { fn generate_key(len: usize, rng: impl RngCore) -> Result; fn commit( @@ -79,28 +81,28 @@ pub trait VectorCommitmentOps: VectorCommitmentDef { ) -> Result<(), Error>; } -pub trait VectorCommitmentDefGadget: Clone { +pub trait CommitmentDefGadget: Clone { type ConstraintField: SonobeField; type KeyVar; type ScalarVar: AbsorbableGadget + CondSelectGadget + FromBitsGadget - + AllocVar<::Scalar, Self::ConstraintField> - + GR1CSVar::Scalar> + + AllocVar<::Scalar, Self::ConstraintField> + + GR1CSVar::Scalar> + TwoStageFieldVar; type CommitmentVar: Clone + AbsorbableGadget + CondSelectGadget - + AllocVar<::Commitment, Self::ConstraintField> - + GR1CSVar::Commitment>; - type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> - + GR1CSVar::Randomness>; + + AllocVar<::Commitment, Self::ConstraintField> + + GR1CSVar::Commitment>; + type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> + + GR1CSVar::Randomness>; - type Native: VectorCommitmentDef; + type Native: CommitmentDef; } -pub trait VectorCommitmentOpsGadget: VectorCommitmentDefGadget { +pub trait CommitmentOpsGadget: CommitmentDefGadget { fn open( ck: &Self::KeyVar, v: &[Self::ScalarVar], @@ -109,25 +111,23 @@ pub trait VectorCommitmentOpsGadget: VectorCommitmentDefGadget { ) -> Result<(), SynthesisError>; } -pub trait GroupBasedVectorCommitment: - VectorCommitmentDef< - Commitment: SonobeCurve, - Scalar = CF1<::Commitment>, - > + VectorCommitmentOps +pub trait GroupBasedCommitment: + CommitmentDef::Commitment>> + + CommitmentOps { - type Gadget1: VectorCommitmentOpsGadget - + VectorCommitmentDefGadget< + type Gadget1: CommitmentOpsGadget + + CommitmentDefGadget< ConstraintField = CF2, ScalarVar = EmulatedFieldVar, Self::Scalar>, CommitmentVar = ::Var, Native = Self, >; - type Gadget2: VectorCommitmentDefGadget< - ConstraintField = Self::Scalar, - ScalarVar = FpVar, - CommitmentVar = EmulatedAffineVar, - Native = Self, - >; + type Gadget2: CommitmentDefGadget< + ConstraintField = Self::Scalar, + ScalarVar = FpVar, + CommitmentVar = EmulatedAffineVar, + Native = Self, + >; } #[cfg(test)] @@ -137,17 +137,17 @@ mod tests { use super::*; - pub fn test_commitment_correctness( + pub fn test_commitment_correctness( mut rng: impl RngCore, len: usize, ) -> Result<(), Box> { let v = (0..len) - .map(|_| VC::Scalar::rand(&mut rng)) + .map(|_| CM::Scalar::rand(&mut rng)) .collect::>(); - let ck = VC::generate_key(len, &mut rng)?; - let (cm, r) = VC::commit(&ck, &v, &mut rng)?; - VC::open(&ck, &v, &r, &cm)?; + let ck = CM::generate_key(len, &mut rng)?; + let (cm, r) = CM::commit(&ck, &v, &mut rng)?; + CM::open(&ck, &v, &r, &cm)?; Ok(()) } } diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 7d470b933..381bb74eb 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -2,15 +2,13 @@ use ark_r1cs_std::{ boolean::Boolean, convert::ToBitsGadget, eq::EqGadget, fields::fp::FpVar, groups::CurveVar, }; use ark_relations::gr1cs::SynthesisError; -use ark_std::{iter::repeat_with, marker::PhantomData, rand::RngCore, UniformRand}; +use ark_std::{UniformRand, iter::repeat_with, marker::PhantomData, rand::RngCore}; -use super::{ - CommitmentKey, Error, VectorCommitmentDef, VectorCommitmentDefGadget, VectorCommitmentOps, -}; +use super::{CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, Error}; use crate::{ algebra::{field::emulated::EmulatedFieldVar, group::emulated::EmulatedAffineVar}, - commitments::{GroupBasedVectorCommitment, VectorCommitmentOpsGadget}, - traits::{SonobeCurve, CF1, CF2}, + commitments::{CommitmentOpsGadget, GroupBasedCommitment}, + traits::{CF1, CF2, SonobeCurve}, utils::null::Null, }; @@ -70,7 +68,7 @@ pub struct PedersenEmulatedGadget { _c: PhantomData, } -impl VectorCommitmentDef for Pedersen { +impl CommitmentDef for Pedersen { const IS_HIDING: bool = false; type Key = PedersenKey; @@ -79,7 +77,7 @@ impl VectorCommitmentDef for Pedersen { type Randomness = Null; } -impl VectorCommitmentDef for Pedersen { +impl CommitmentDef for Pedersen { const IS_HIDING: bool = true; type Key = PedersenKey; @@ -88,7 +86,7 @@ impl VectorCommitmentDef for Pedersen { type Randomness = C::ScalarField; } -impl VectorCommitmentDefGadget for PedersenGadget { +impl CommitmentDefGadget for PedersenGadget { type ConstraintField = CF2; type KeyVar = Vec; @@ -102,7 +100,7 @@ impl VectorCommitmentDefGadget for PedersenGadget { type Native = Pedersen; } -impl VectorCommitmentDefGadget for PedersenGadget { +impl CommitmentDefGadget for PedersenGadget { type ConstraintField = CF2; type KeyVar = (Vec, C::Var); @@ -116,7 +114,7 @@ impl VectorCommitmentDefGadget for PedersenGadget { type Native = Pedersen; } -impl VectorCommitmentDefGadget for PedersenEmulatedGadget { +impl CommitmentDefGadget for PedersenEmulatedGadget { type ConstraintField = CF1; type KeyVar = Vec, C>>; @@ -130,7 +128,7 @@ impl VectorCommitmentDefGadget for PedersenEmulatedGadget; } -impl VectorCommitmentDefGadget for PedersenEmulatedGadget { +impl CommitmentDefGadget for PedersenEmulatedGadget { type ConstraintField = CF1; type KeyVar = ( @@ -147,17 +145,17 @@ impl VectorCommitmentDefGadget for PedersenEmulatedGadget; } -impl GroupBasedVectorCommitment for Pedersen { +impl GroupBasedCommitment for Pedersen { type Gadget1 = PedersenGadget; type Gadget2 = PedersenEmulatedGadget; } -impl GroupBasedVectorCommitment for Pedersen { +impl GroupBasedCommitment for Pedersen { type Gadget1 = PedersenGadget; type Gadget2 = PedersenEmulatedGadget; } -impl VectorCommitmentOps for Pedersen { +impl CommitmentOps for Pedersen { fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { Ok(PedersenKey::new(len, rng)) } @@ -177,7 +175,7 @@ impl VectorCommitmentOps for Pedersen { } } -impl VectorCommitmentOps for Pedersen { +impl CommitmentOps for Pedersen { fn generate_key(len: usize, rng: impl RngCore) -> Result, Error> { Ok(PedersenKey::new(len, rng)) } @@ -313,7 +311,7 @@ impl PedersenGadget { } } -impl VectorCommitmentOpsGadget for PedersenGadget { +impl CommitmentOpsGadget for PedersenGadget { fn open( ck: &Vec, v: &[EmulatedFieldVar, CF1>], @@ -330,7 +328,7 @@ impl VectorCommitmentOpsGadget for PedersenGadget { } } -impl VectorCommitmentOpsGadget for PedersenGadget { +impl CommitmentOpsGadget for PedersenGadget { fn open( (g, h): &(Vec, C::Var), v: &[EmulatedFieldVar, CF1>], diff --git a/crates/primitives/src/sumcheck/circuits.rs b/crates/primitives/src/sumcheck/circuits.rs index 640e59725..c8f8ce2f5 100644 --- a/crates/primitives/src/sumcheck/circuits.rs +++ b/crates/primitives/src/sumcheck/circuits.rs @@ -6,7 +6,7 @@ use ark_ff::PrimeField; use ark_r1cs_std::{ eq::EqGadget, - fields::{fp::FpVar, FieldVar}, + fields::{FieldVar, fp::FpVar}, poly::polynomial::univariate::dense::DensePolynomialVar, }; use ark_relations::gr1cs::SynthesisError; @@ -56,21 +56,21 @@ impl IOPSumCheckGadget { mod tests { use ark_bn254::Fr; use ark_crypto_primitives::sponge::{ - poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, CryptographicSponge, + poseidon::{PoseidonSponge, constraints::PoseidonSpongeVar}, }; use ark_ff::{One, Zero}; use ark_poly::{ - univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, - MultilinearExtension, Polynomial, + DenseMultilinearExtension, DenseUVPolynomial, MultilinearExtension, Polynomial, + univariate::DensePolynomial, }; - use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; + use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, test_rng}; use super::*; use crate::{ - sumcheck::{utils::VirtualPolynomial, IOPSumCheck}, + sumcheck::{IOPSumCheck, utils::VirtualPolynomial}, transcripts::poseidon::poseidon_canonical_config, }; diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index 610102b4e..29259e6c4 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -11,7 +11,7 @@ use ark_ff::PrimeField; use ark_poly::{ - univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, Polynomial, + DenseMultilinearExtension, DenseUVPolynomial, Polynomial, univariate::DensePolynomial, }; use ark_std::{cfg_chunks, cfg_into_iter, fmt::Debug}; #[cfg(feature = "parallel")] @@ -19,8 +19,8 @@ use rayon::prelude::*; use thiserror::Error; use self::utils::{ - barycentric_weights, compute_lagrange_interpolated_poly, extrapolate, VPAuxInfo, - VirtualPolynomial, + VPAuxInfo, VirtualPolynomial, barycentric_weights, compute_lagrange_interpolated_poly, + extrapolate, }; use crate::transcripts::{Absorbable, Transcript}; @@ -211,7 +211,7 @@ pub mod tests { use ark_ff::Field; use ark_pallas::Fr; use ark_poly::MultilinearExtension; - use ark_std::{test_rng, One, Zero}; + use ark_std::{One, Zero, test_rng}; use super::*; use crate::transcripts::poseidon::poseidon_canonical_config; diff --git a/crates/primitives/src/sumcheck/utils.rs b/crates/primitives/src/sumcheck/utils.rs index 2cc71f748..6d3ad719b 100644 --- a/crates/primitives/src/sumcheck/utils.rs +++ b/crates/primitives/src/sumcheck/utils.rs @@ -10,9 +10,9 @@ //! This module defines our main mathematical object `VirtualPolynomial`; and //! various functions associated with it. -use ark_ff::{batch_inversion, Field, PrimeField}; -use ark_poly::{univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial}; -use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use ark_ff::{Field, PrimeField, batch_inversion}; +use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, univariate::DensePolynomial}; +use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; use ark_serialize::CanonicalSerialize; use ark_std::cfg_into_iter; #[cfg(feature = "parallel")] @@ -207,7 +207,7 @@ pub fn compute_lagrange_interpolated_poly(p_i: &[F]) -> DensePoly #[cfg(test)] mod tests { use ark_pallas::Fr; - use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; + use ark_poly::{DenseUVPolynomial, Polynomial, univariate::DensePolynomial}; use ark_std::UniformRand; use super::*; diff --git a/crates/primitives/src/traits.rs b/crates/primitives/src/traits.rs index 9c97b277f..6162c5c4c 100644 --- a/crates/primitives/src/traits.rs +++ b/crates/primitives/src/traits.rs @@ -1,6 +1,6 @@ pub use crate::algebra::{ field::SonobeField, - group::{SonobeCurve, CF1, CF2}, + group::{CF1, CF2, SonobeCurve}, }; pub trait Dummy { diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs index 1b7134234..fa706085d 100644 --- a/crates/primitives/src/transcripts/griffin/mod.rs +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -1,14 +1,14 @@ use ark_ff::{LegendreSymbol, PrimeField}; use ark_r1cs_std::{ - alloc::AllocVar, - fields::{fp::FpVar, FieldVar}, GR1CSVar, + alloc::AllocVar, + fields::{FieldVar, fp::FpVar}, }; use ark_relations::gr1cs::SynthesisError; use num_bigint::BigUint; use sha3::{ - digest::{ExtendableOutput, Update, XofReader}, Shake128, Shake128Reader, + digest::{ExtendableOutput, Update, XofReader}, }; pub mod sponge; diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs index fc732eadf..7205b74d7 100644 --- a/crates/primitives/src/transcripts/griffin/sponge.rs +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -1,13 +1,13 @@ use ark_crypto_primitives::sponge::DuplexSpongeMode; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{ - fields::{fp::FpVar, FieldVar}, + fields::{FieldVar, fp::FpVar}, prelude::{Boolean, ToBitsGadget}, }; use ark_relations::gr1cs::SynthesisError; use ark_std::sync::Arc; -use crate::transcripts::{griffin::GriffinParams, AbsorbableGadget, Transcript, TranscriptVar}; +use crate::transcripts::{AbsorbableGadget, Transcript, TranscriptVar, griffin::GriffinParams}; #[derive(Clone)] pub struct GriffinSponge { @@ -350,14 +350,14 @@ impl TranscriptVar for GriffinSpongeVar { #[cfg(test)] pub mod tests { - use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; + use ark_bn254::{Fq, Fr, G1Projective as G1, constraints::GVar, g1::Config}; use ark_ec::PrimeGroup; use ark_ff::UniformRand; use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, - groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, - GR1CSVar, + groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, }; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, test_rng}; diff --git a/crates/primitives/src/transcripts/poseidon/sponge.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs index f1f58cc76..5110fe4bb 100644 --- a/crates/primitives/src/transcripts/poseidon/sponge.rs +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -1,7 +1,7 @@ use ark_crypto_primitives::sponge::{ - constraints::CryptographicSpongeVar, - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, Absorb, CryptographicSponge, FieldBasedCryptographicSponge, + constraints::CryptographicSpongeVar, + poseidon::{PoseidonConfig, PoseidonSponge, constraints::PoseidonSpongeVar}, }; use ark_ff::PrimeField; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; @@ -76,15 +76,15 @@ impl TranscriptVar for PoseidonSpongeVar { #[cfg(test)] pub mod tests { - use ark_bn254::{constraints::GVar, g1::Config, Fq, Fr, G1Projective as G1}; - use ark_crypto_primitives::sponge::poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}; + use ark_bn254::{Fq, Fr, G1Projective as G1, constraints::GVar, g1::Config}; + use ark_crypto_primitives::sponge::poseidon::{PoseidonSponge, constraints::PoseidonSpongeVar}; use ark_ec::PrimeGroup; use ark_ff::UniformRand; use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, - groups::{curves::short_weierstrass::ProjectiveVar, CurveVar}, - GR1CSVar, + groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, }; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, str::FromStr, test_rng}; diff --git a/crates/primitives/src/utils/null.rs b/crates/primitives/src/utils/null.rs index 6d043534b..bc6a9bb09 100644 --- a/crates/primitives/src/utils/null.rs +++ b/crates/primitives/src/utils/null.rs @@ -1,7 +1,7 @@ use ark_ff::Field; use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, GR1CSVar, + alloc::{AllocVar, AllocationMode}, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{ From 1dbdf21119625027ae9ddc62b75e0e05deb8e8c5 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 16:19:13 +0800 Subject: [PATCH 21/93] Fix CI --- crates/primitives/Cargo.toml | 3 +++ crates/primitives/src/transcripts/griffin/sponge.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index bf252b6a2..ab95fe398 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -26,6 +26,9 @@ thiserror = { workspace = true } ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } ark-pallas = { workspace = true, features = ["curve", "r1cs"] } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + [features] default = ["parallel"] parallel = [ diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs index 7205b74d7..2181a42ad 100644 --- a/crates/primitives/src/transcripts/griffin/sponge.rs +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -26,7 +26,7 @@ impl GriffinSponge { self.griffin.permute(&mut self.state); } - // Absorbs everything in elements, this does not end in an absorbtion. + // Absorbs everything in elements, this does not end in an absorption. fn absorb_internal(&mut self, mut rate_start_index: usize, elements: &[F]) { let mut remaining_elements = elements; From da29e88845ae12ec113b2e684d40dc0834c758a9 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 16:44:30 +0800 Subject: [PATCH 22/93] Fix clippy --- crates/primitives/src/algebra/field/emulated.rs | 4 ++-- crates/primitives/src/sumcheck/mod.rs | 1 + crates/primitives/src/transcripts/griffin/mod.rs | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs index a4c11e64f..b22fd8ad7 100644 --- a/crates/primitives/src/algebra/field/emulated.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -458,8 +458,7 @@ impl IntVarInner AllocVar<(BigInt, Bound), F> for IntVarInner= lb` + #[allow(clippy::if_same_then_else)] if lb.is_zero() && ub + BigInt::one() == BigInt::one() << len { } else if BigInt::one() - lb == BigInt::one() << len && ub.is_zero() { } else if BigInt::one() - lb == BigInt::one() << len diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index 29259e6c4..4a58a89b8 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -41,6 +41,7 @@ pub enum Error { pub struct IOPSumCheck; impl IOPSumCheck { + #[allow(clippy::type_complexity)] pub fn prove( mut poly: VirtualPolynomial, transcript: &mut impl Transcript, diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs index fa706085d..ca80f4642 100644 --- a/crates/primitives/src/transcripts/griffin/mod.rs +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -50,7 +50,9 @@ impl GriffinParams { pub const INIT_SHAKE: &'static str = "Griffin"; pub fn new(t: usize, d: usize, rounds: usize) -> Self { - assert!(t == 3 || t % 4 == 0); + // Equivalent to `assert!(t == 3 || t % 4 == 0);`, but bypass clippy's + // warning about `is_multiple_of`. + assert!(t == 3 || t & 3 == 0); assert!(d == 3 || d == 5); assert!(rounds >= 1); From 11fb8b194c240c4bf99e367f6c22cb36f95cd633 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 18:35:53 +0800 Subject: [PATCH 23/93] Actually test wasm targets --- Cargo.toml | 50 +++++++------------ crates/primitives/Cargo.toml | 5 +- .../primitives/src/algebra/field/emulated.rs | 14 +++--- .../primitives/src/algebra/group/emulated.rs | 8 +-- .../src/arithmetizations/r1cs/mod.rs | 10 ++-- crates/primitives/src/commitments/pedersen.rs | 9 +++- crates/primitives/src/sumcheck/circuits.rs | 6 ++- crates/primitives/src/sumcheck/mod.rs | 6 ++- crates/primitives/src/sumcheck/utils.rs | 8 +-- .../primitives/src/transcripts/griffin/mod.rs | 2 + .../src/transcripts/griffin/sponge.rs | 8 +-- .../src/transcripts/poseidon/sponge.rs | 8 +-- 12 files changed, 72 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c8772be8..6d1e89a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,49 +33,33 @@ repository = "https://github.com/privacy-scaling-explorations/sonobe/" rust-version = "1.85.1" [workspace.dependencies] -acvm = { git = "https://github.com/winderica/noir", rev = "fc9e99", default-features = false } # "arkworks-next" branch -askama = { version = "0.12.0", default-features = false } -clap = { version = "4.4" } -clap-verbosity-flag = { version = "2.1" } -criterion = { version = "0.5" } -env_logger = { version = "0.10" } -getrandom = { version = "0.2" } -log = { version = "0.4" } -noname = { git = "https://github.com/dmpierre/noname", rev = "c34f17" } num-bigint = { version = "0.4.3" } num-integer = { version = "0.1" } num-traits = { version = "0.2" } -pprof = { version = "0.13" } -serde = { version = "^1.0.0" } -serde_json = { version = "^1.0.0" } sha3 = { version = "0.10" } rand = { version = "0.8.5" } rayon = { version = "1" } -revm = { version = "19.5.0", default-features = false } -rust-crypto = { version = "0.2" } -thiserror = { version = "1.0" } -tokio = "1.44.1" -wasmer = { version = "6.1.0", default-features = false } +thiserror = { version = "2.0.16" } +wasm-bindgen-test = { version = "0.3" } # Arkworks family -ark-bn254 = { version = "^0.5.0", default-features = false } -ark-circom = { git = "https://github.com/arkworks-rs/circom-compat", default-features = false } -ark-crypto-primitives = { version = "^0.5.0", default-features = false } -ark-ec = { version = "^0.5.0", default-features = false } -ark-ff = { version = "^0.5.0", default-features = false } -ark-groth16 = { git = "https://github.com/arkworks-rs/groth16" } -ark-grumpkin = { version = "^0.5.0", default-features = false } -ark-mnt4-298 = { version = "^0.5.0" } -ark-mnt6-298 = { version = "^0.5.0" } -ark-pallas = { version = "^0.5.0" } -ark-poly = { version = "^0.5.0", default-features = false } -ark-poly-commit = { version = "^0.5.0" } -ark-r1cs-std = { version = "^0.5.0", default-features = false } +ark-crypto-primitives = { git = "https://github.com/arkworks-rs/crypto-primitives", default-features = false } +ark-ec = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-ff = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-groth16 = { git = "https://github.com/arkworks-rs/groth16", default-features = false } +ark-poly = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-poly-commit = { git = "https://github.com/arkworks-rs/poly-commit", default-features = false } +ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std", default-features = false } ark-relations = { git = "https://github.com/arkworks-rs/snark", default-features = false } -ark-serialize = { version = "^0.5.0" } +ark-serialize = { git = "https://github.com/arkworks-rs/algebra", default-features = false } ark-snark = { git = "https://github.com/arkworks-rs/snark", default-features = false } -ark-std = { version = "^0.5.0", default-features = false } -ark-vesta = { version = "^0.5.0" } +ark-std = { git = "https://github.com/arkworks-rs/std", default-features = false } + +# Ark curves +ark-bn254 = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-grumpkin = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-pallas = { git = "https://github.com/arkworks-rs/algebra", default-features = false } +ark-vesta = { git = "https://github.com/arkworks-rs/algebra", default-features = false } # Local crates sonobe-primitives = { path = "crates/primitives", default-features = false } diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index ab95fe398..15b3e513f 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -29,8 +29,11 @@ ark-pallas = { workspace = true, features = ["curve", "r1cs"] } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] getrandom = { version = "0.2", features = ["js"] } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = { workspace = true } + [features] -default = ["parallel"] +default = [] parallel = [ "ark-poly-commit/parallel", "ark-relations/parallel", diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs index b22fd8ad7..1081044e8 100644 --- a/crates/primitives/src/algebra/field/emulated.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -1147,14 +1147,16 @@ mod tests { use ark_ff::Field; use ark_pallas::{Fq, Fr}; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{UniformRand, error::Error, test_rng}; + use ark_std::{UniformRand, error::Error, rand::thread_rng}; use num_bigint::RandBigInt; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; #[test] fn test_alloc() -> Result<(), Box> { - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let size = 1024; let mut lbs = vec![BigInt::zero()]; @@ -1211,7 +1213,7 @@ mod tests { let size = 2048; - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let a = rng.gen_bigint(size as u64); let b = rng.gen_bigint(size as u64); let ab = &a * &b; @@ -1282,7 +1284,7 @@ mod tests { fn test_mul_fq() -> Result<(), Box> { let cs = ConstraintSystem::::new_ref(); - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let a = Fq::rand(rng); let b = Fq::rand(rng); let ab = a * b; @@ -1307,7 +1309,7 @@ mod tests { fn test_pow() -> Result<(), Box> { let cs = ConstraintSystem::::new_ref(); - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let a = Fq::rand(rng); @@ -1329,7 +1331,7 @@ mod tests { let len = 1000; - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let a = (0..len).map(|_| Fq::rand(rng)).collect::>(); let b = (0..len).map(|_| Fq::rand(rng)).collect::>(); let c = a.iter().zip(b.iter()).map(|(a, b)| a * b).sum::(); diff --git a/crates/primitives/src/algebra/group/emulated.rs b/crates/primitives/src/algebra/group/emulated.rs index 42b93e3e4..477de2b0e 100644 --- a/crates/primitives/src/algebra/group/emulated.rs +++ b/crates/primitives/src/algebra/group/emulated.rs @@ -138,7 +138,9 @@ mod tests { use ark_pallas::{Fq, Fr, PallasConfig, Projective}; use ark_r1cs_std::groups::curves::short_weierstrass::ProjectiveVar; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{UniformRand, error::Error}; + use ark_std::{UniformRand, error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::{ @@ -160,7 +162,7 @@ mod tests { let cs = ConstraintSystem::::new_ref(); // check that point_to_nonnative_limbs returns the expected values - let mut rng = ark_std::test_rng(); + let mut rng = thread_rng(); let p = Projective::rand(&mut rng); let p_var = EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p))?; assert_eq!(p_var.to_absorbable()?.value()?, p.to_absorbable()); @@ -170,7 +172,7 @@ mod tests { #[test] fn test_inputize() -> Result<(), Box> { // check that point_to_nonnative_limbs returns the expected values - let mut rng = ark_std::test_rng(); + let mut rng = thread_rng(); let p = Projective::rand(&mut rng); let cs = ConstraintSystem::::new_ref(); diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 80d6cf8d3..84c1c6278 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -249,7 +249,9 @@ pub mod tests { use ark_bn254::Fr; use ark_ff::UniformRand; use ark_relations::gr1cs::ConstraintSynthesizer; - use ark_std::{error::Error, test_rng}; + use ark_std::{error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::circuits::{ @@ -259,7 +261,7 @@ pub mod tests { #[test] fn test_satisfiability() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); let circuit = CircuitForTest:: { x: Fr::rand(&mut rng), }; @@ -272,7 +274,7 @@ pub mod tests { #[test] fn test_constraint_extraction() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); let circuit = CircuitForTest:: { x: Fr::rand(&mut rng), }; @@ -284,7 +286,7 @@ pub mod tests { #[test] fn test_witness_extraction() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); let x = Fr::rand(&mut rng); let circuit = CircuitForTest:: { x }; diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index 381bb74eb..e51ef8edf 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -349,14 +349,19 @@ impl CommitmentOpsGadget for PedersenGadget { #[cfg(test)] mod tests { use ark_bn254::G1Projective; - use ark_std::{error::Error, rand::Rng, test_rng}; + use ark_std::{ + error::Error, + rand::{Rng, thread_rng}, + }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::commitments::tests::test_commitment_correctness; #[test] fn test_pedersen_commitment() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); for i in 0..10 { let len = rng.gen_range((1 << i)..(1 << (i + 1))); test_commitment_correctness::>(&mut rng, len)?; diff --git a/crates/primitives/src/sumcheck/circuits.rs b/crates/primitives/src/sumcheck/circuits.rs index c8f8ce2f5..b79967e9e 100644 --- a/crates/primitives/src/sumcheck/circuits.rs +++ b/crates/primitives/src/sumcheck/circuits.rs @@ -66,7 +66,9 @@ mod tests { }; use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{error::Error, test_rng}; + use ark_std::{error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::{ @@ -76,7 +78,7 @@ mod tests { #[test] fn test_sum_check_circuit() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); let poseidon_config = poseidon_canonical_config::(); for num_vars in 1..15 { let mut transcript_p = PoseidonSponge::new(&poseidon_config); diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index 4a58a89b8..d42d78a1c 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -212,7 +212,9 @@ pub mod tests { use ark_ff::Field; use ark_pallas::Fr; use ark_poly::MultilinearExtension; - use ark_std::{One, Zero, test_rng}; + use ark_std::{One, Zero, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::transcripts::poseidon::poseidon_canonical_config; @@ -221,7 +223,7 @@ pub mod tests { pub fn sumcheck_poseidon() -> Result<(), Error> { let n_vars = 10; - let mut rng = test_rng(); + let mut rng = thread_rng(); let poly_mle = DenseMultilinearExtension::rand(n_vars, &mut rng); let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); diff --git a/crates/primitives/src/sumcheck/utils.rs b/crates/primitives/src/sumcheck/utils.rs index 6d3ad719b..917ba6d2e 100644 --- a/crates/primitives/src/sumcheck/utils.rs +++ b/crates/primitives/src/sumcheck/utils.rs @@ -208,7 +208,9 @@ pub fn compute_lagrange_interpolated_poly(p_i: &[F]) -> DensePoly mod tests { use ark_pallas::Fr; use ark_poly::{DenseUVPolynomial, Polynomial, univariate::DensePolynomial}; - use ark_std::UniformRand; + use ark_std::{UniformRand, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; @@ -351,7 +353,7 @@ mod tests { #[test] fn test_compute_lagrange_interpolated_poly() { - let mut prng = ark_std::test_rng(); + let mut prng = thread_rng(); for degree in 1..30 { let poly = DensePolynomial::::rand(degree, &mut prng); // range (which is exclusive) is from 0 to degree + 1, since we need degree + 1 evaluations @@ -371,7 +373,7 @@ mod tests { #[test] fn test_interpolation() { - let mut prng = ark_std::test_rng(); + let mut prng = thread_rng(); // test a polynomial with 20 known points, i.e., with degree 19 let poly = DensePolynomial::::rand(20 - 1, &mut prng); diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs index ca80f4642..518d69547 100644 --- a/crates/primitives/src/transcripts/griffin/mod.rs +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -458,6 +458,8 @@ mod tests { use ark_ff::UniformRand; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs index 2181a42ad..ecf2e75e5 100644 --- a/crates/primitives/src/transcripts/griffin/sponge.rs +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -360,7 +360,9 @@ pub mod tests { groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, }; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{error::Error, test_rng}; + use ark_std::{error::Error, rand::thread_rng}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::algebra::{group::emulated::EmulatedAffineVar, ops::bits::FromBits}; @@ -370,7 +372,7 @@ pub mod tests { // use 'native' transcript let config = Arc::new(GriffinParams::::new(3, 5, 12)); let mut tr = GriffinSponge::::new(&config); - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let p = G1::rand(rng); tr.add(&p); @@ -393,7 +395,7 @@ pub mod tests { // use 'native' transcript let config = Arc::new(GriffinParams::::new(3, 5, 12)); let mut tr = GriffinSponge::::new(&config); - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let p = G1::rand(rng); tr.add(&p); diff --git a/crates/primitives/src/transcripts/poseidon/sponge.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs index 5110fe4bb..e595247c8 100644 --- a/crates/primitives/src/transcripts/poseidon/sponge.rs +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -87,7 +87,9 @@ pub mod tests { groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, }; use ark_relations::gr1cs::ConstraintSystem; - use ark_std::{error::Error, str::FromStr, test_rng}; + use ark_std::{error::Error, rand::thread_rng, str::FromStr}; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use crate::{ algebra::{group::emulated::EmulatedAffineVar, ops::bits::FromBits}, @@ -120,7 +122,7 @@ pub mod tests { // use 'native' transcript let config = poseidon_canonical_config::(); let mut tr = PoseidonSponge::::new(&config); - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let p = G1::rand(rng); tr.add(&p); @@ -143,7 +145,7 @@ pub mod tests { // use 'native' transcript let config = poseidon_canonical_config::(); let mut tr = PoseidonSponge::::new(&config); - let rng = &mut test_rng(); + let rng = &mut thread_rng(); let p = G1::rand(rng); tr.add(&p); From 0f4a90dce159cde80dfb28edf0cd2fba8b2ee049 Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 9 Feb 2026 19:36:40 +0800 Subject: [PATCH 24/93] Add docs for primitives --- .../primitives/src/algebra/field/emulated.rs | 415 +++++++++++------- crates/primitives/src/algebra/field/mod.rs | 50 ++- .../primitives/src/algebra/group/emulated.rs | 42 +- crates/primitives/src/algebra/group/mod.rs | 85 +--- crates/primitives/src/algebra/mod.rs | 22 +- crates/primitives/src/algebra/ops/bits.rs | 34 +- crates/primitives/src/algebra/ops/eq.rs | 14 +- crates/primitives/src/algebra/ops/matrix.rs | 20 +- crates/primitives/src/algebra/ops/mod.rs | 11 + crates/primitives/src/algebra/ops/poly.rs | 21 +- crates/primitives/src/algebra/ops/pow.rs | 22 +- crates/primitives/src/algebra/ops/rlc.rs | 18 + crates/primitives/src/algebra/ops/vector.rs | 42 +- .../src/arithmetizations/ccs/circuits.rs | 13 +- .../src/arithmetizations/ccs/mod.rs | 78 ++-- crates/primitives/src/arithmetizations/mod.rs | 203 ++++----- .../src/arithmetizations/r1cs/circuits.rs | 52 ++- .../src/arithmetizations/r1cs/mod.rs | 87 ++-- crates/primitives/src/circuits/mod.rs | 126 +++++- crates/primitives/src/circuits/utils.rs | 17 +- crates/primitives/src/commitments/mod.rs | 103 ++++- crates/primitives/src/commitments/pedersen.rs | 239 ++++------ crates/primitives/src/lib.rs | 10 + crates/primitives/src/relations/mod.rs | 27 +- crates/primitives/src/sumcheck/circuits.rs | 81 +++- crates/primitives/src/sumcheck/mod.rs | 114 ++++- crates/primitives/src/sumcheck/utils.rs | 86 ++-- crates/primitives/src/traits.rs | 32 +- .../primitives/src/transcripts/absorbable.rs | 48 +- .../primitives/src/transcripts/griffin/mod.rs | 322 ++++++++------ .../src/transcripts/griffin/sponge.rs | 240 +++++----- crates/primitives/src/transcripts/mod.rs | 114 ++++- .../src/transcripts/poseidon/mod.rs | 9 +- .../src/transcripts/poseidon/sponge.rs | 153 +++---- crates/primitives/src/utils/mod.rs | 2 + crates/primitives/src/utils/null.rs | 8 + 36 files changed, 1838 insertions(+), 1122 deletions(-) diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs index 1081044e8..d75d61923 100644 --- a/crates/primitives/src/algebra/field/emulated.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -1,3 +1,14 @@ +//! This module provides implementation of in-circuit variables for emulated +//! integers or field elements. +//! +//! This is useful when we want to express or perform operations over a ring or +//! field in a circuit defined over a different field. +//! +//! Note that the implementation here is dedicated to Sonobe's use cases and the +//! priorities are efficiency instead of generality or usability, e.g., the user +//! needs to manually ensure the variables do not overflow the field capacity. +//! Therefore, be cautious if you want to use it in other contexts. + use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_r1cs_std::{ GR1CSVar, @@ -27,30 +38,50 @@ use crate::{ bits::{FromBitsGadget, ToBitsGadgetExt}, eq::EquivalenceGadget, matrix::{MatrixGadget, SparseMatrixVar}, - vector::VectorGadget, }, }, - transcripts::AbsorbableGadget, + transcripts::AbsorbableVar, }; +/// [`Bounds`] records the lower and upper bounds (inclusive) of an integer. +/// +/// When allocating an emulated field element, we need to decompose it into +/// several limbs, each represented as a variable in the constraint field. +/// Operations over the emulated field element are translated into operations +/// over its limbs. +/// After several operations, the limbs may grow larger than the capacity of the +/// constraint field, and to prevent that, we track the bounds of each limb +/// using this struct, so that we can take action before the limbs overflow. #[derive(Debug, Default, Clone, PartialEq)] -pub struct Bound(pub BigInt, pub BigInt); +pub struct Bounds(pub BigInt, pub BigInt); -impl Bound { +impl Bounds { + /// [`Bounds::zero`] returns the bounds `[0, 0]`. pub fn zero() -> Self { Self::default() } } -impl Bound { +impl Bounds { + /// [`Bounds::add`] computes the sum of two pairs of bounds. pub fn add(&self, other: &Self) -> Self { + // Consider two values `x` and `y`. + // For `z = x + y`, its lower bound is the sum of the lower bounds of + // `x` and `y`, and its upper bound is the sum of the upper bounds of + // `x` and `y`. Self(&self.0 + &other.0, &self.1 + &other.1) } + /// [`Bounds::sub`] computes the difference of two pairs of bounds. pub fn sub(&self, other: &Self) -> Self { + // Consider two values `x` and `y`. + // For `z = x - y`, its lower bound is the difference of the lower bound + // of `x` and the upper bound of `y`, and its upper bound is the + // difference of the upper bound of `x` and the lower bound of `y`. Self(&self.0 - &other.1, &self.1 - &other.0) } + /// [`Bounds::add_many`] computes the sum of multiple pairs of bounds. pub fn add_many(limbs: &[Self]) -> Self { Self( limbs.iter().map(|l| &l.0).sum(), @@ -58,29 +89,45 @@ impl Bound { ) } + /// [`Bounds::mul`] computes the product of two pairs of bounds. pub fn mul(&self, other: &Self) -> Self { + // Consider two values `x` and `y`. + // To compute the bounds of `z = x * y`, we need to take into account + // the signs of `x` and `y`. + // + // Therefore, we first compute the following 4 products formed by the + // possible combinations of the bounds of `x` and `y`: let ll = &self.0 * &other.0; let lu = &self.0 * &other.1; let ul = &self.1 * &other.0; let uu = &self.1 * &other.1; + // `z`'s lower bound is the minimum of these products, and its upper + // bound is the maximum of these products. Self( min(min(&ll, &lu), min(&ul, &uu)).clone(), max(max(&ll, &lu), max(&ul, &uu)).clone(), ) } + /// [`Bounds::shl`] computes the bounds after left-shifting by `shift` bits. pub fn shl(&self, shift: usize) -> Self { + // Given `x`, the bounds of `x << shift` can simply be computed by + // shifting the bounds of `x`. Self(&self.0 << shift, &self.1 << shift) } + /// [`Bounds::filter_safe`] checks if the bounds fit within the capacity of + /// a prime field `F`, and returns `Some(self)` if so, or `None` otherwise. pub fn filter_safe(self) -> Option { + // For a field `F`, we consider an integer `x` to be safe if and only if + // `-(|F| - 1) / 2 <= x <= (|F| - 1) / 2`. let limit = BigInt::from_biguint(Sign::Plus, F::MODULUS_MINUS_ONE_DIV_TWO.into()); (self.0 >= -&limit && self.1 <= limit).then_some(self) } } -fn compose>(limbs: V) -> BigInt { +fn compose(limbs: impl Borrow<[F]>) -> BigInt { let mut r = BigInt::zero(); for &limb in limbs.borrow().iter().rev() { @@ -94,18 +141,44 @@ fn compose>(limbs: V) -> BigInt { r } +/// [`LimbedVar`] represents an in-circuit variable for an emulated integer or +/// field element, whose value is decomposed into several limbs, each being +/// created as a [`FpVar`] in the constraint field and tracked with its bounds. +/// +/// The generic parameter `Cfg` can be used to customize the behavior of ops on +/// `LimbedVar`, for instance, by specifying the modulus when emulating a field +/// element. +/// +/// The const generic parameter `ALIGNED` indicates if the limbs are "aligned". +/// When allocating a [`LimbedVar`], each limb has a predefined bit-length, but +/// after several operations, the actual bit-length of each limb may grow beyond +/// that. +/// It is usually fine to have larger limbs, but if they becomes larger than the +/// field capacity, we can no longer do operations on them. +/// Therefore, we sometimes need to "align" the limbs, i.e., reduce each limb +/// back to the predefined bit-length. +/// We say the limbs are "aligned" if the actual bit-length of each limb equals +/// the predefined bit-length, and "unaligned" otherwise. #[derive(Debug, Clone)] -pub struct IntVarInner { +pub struct LimbedVar { _cfg: PhantomData, - pub limbs: Vec>, - pub bounds: Vec, + pub(crate) limbs: Vec>, + bounds: Vec, } -pub type BigIntVar = IntVarInner; -pub type EmulatedFieldVar = IntVarInner; +/// [`EmulatedIntVar`] is a type alias for emulated integer variables. +/// +/// We only expose aligned variables because unaligned integer variables only +/// appear as intermediate results during computations. +pub type EmulatedIntVar = LimbedVar; +/// [`EmulatedFieldVar`] is a type alias for emulated field element variables. +/// +/// We only expose aligned variables because unaligned integer variables only +/// appear as intermediate results during computations. +pub type EmulatedFieldVar = LimbedVar; -impl GR1CSVar for IntVarInner { - type Value = BigInt; +impl GR1CSVar for LimbedVar { + type Value = BigInt; // For integers, their values are `BigInt`. fn cs(&self) -> ConstraintSystemRef { self.limbs.cs() @@ -117,9 +190,9 @@ impl GR1CSVar for IntVarInner GR1CSVar - for IntVarInner + for LimbedVar { - type Value = Target; + type Value = Target; // For field elements, their values are in `Target`. fn cs(&self) -> ConstraintSystemRef { self.limbs.cs() @@ -138,8 +211,10 @@ impl GR1CSVar } } -impl IntVarInner { - pub fn new(limbs: Vec>, bounds: Vec) -> Self { +impl LimbedVar { + /// [`LimbedVar::new`] creates a new [`LimbedVar`] from the pre-allocated + /// limbs and their bounds. + pub fn new(limbs: Vec>, bounds: Vec) -> Self { Self { _cfg: PhantomData, limbs, @@ -147,6 +222,8 @@ impl IntVarInner { } } + /// [`LimbedVar::ubound`] computes the upper bound of the represented value + /// from the upper bounds of its limbs. fn ubound(&self) -> BigInt { let mut r = BigInt::zero(); @@ -158,6 +235,8 @@ impl IntVarInner { r } + /// [`LimbedVar::lbound`] computes the lower bound of the represented value + /// from the lower bounds of its limbs. fn lbound(&self) -> BigInt { let mut r = BigInt::zero(); @@ -170,10 +249,13 @@ impl IntVarInner { } } -impl IntVarInner { - /// Enforce `self` to be less than `other`, where `self` and `other` should - /// be aligned. - /// Adapted from https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 +impl LimbedVar { + /// [`LimbedVar::enforce_lt`] enforces `self` to be less than `other`, where + /// both should be aligned (as indicated by the const generic). + /// Adapted from the xJsnark [paper] and its [implementation]. + /// + /// [paper]: https://www.cs.yale.edu/homes/cpap/published/xjsnark.pdf + /// [implementation]: https://github.com/akosba/jsnark/blob/0955389d0aae986ceb25affc72edf37a59109250/JsnarkCircuitBuilder/src/circuit/auxiliary/LongElement.java#L801-L872 pub fn enforce_lt(&self, other: &Self) -> Result<(), SynthesisError> { let len = max(self.limbs.len(), other.limbs.len()); let zero = FpVar::zero(); @@ -248,20 +330,21 @@ impl IntVarInner { } } -impl From> for IntVarInner { - fn from(v: IntVarInner) -> Self { +impl From> for LimbedVar { + fn from(v: LimbedVar) -> Self { Self::new(v.limbs, v.bounds) } } -impl IntVarInner { - /// Compute `self + other`, without aligning the limbs. +impl LimbedVar { + /// [`LimbedVar::add_unaligned`] computes `self + other`, without aligning + /// the limbs. pub fn add_unaligned( &self, - other: &IntVarInner, - ) -> Result, SynthesisError> { + other: &LimbedVar, + ) -> Result, SynthesisError> { let mut limbs = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; - let mut bounds = vec![Bound::zero(); limbs.len()]; + let mut bounds = vec![Bounds::zero(); limbs.len()]; for (i, v) in self.limbs.iter().enumerate() { bounds[i] = bounds[i] .add(&self.bounds[i]) @@ -276,15 +359,17 @@ impl IntVarInner( &self, - other: &IntVarInner, - ) -> Result, SynthesisError> { + other: &LimbedVar, + ) -> Result, SynthesisError> { let mut limbs = vec![FpVar::zero(); max(self.limbs.len(), other.limbs.len())]; - let mut bounds = vec![Bound::zero(); limbs.len()]; + let mut bounds = vec![Bounds::zero(); limbs.len()]; for (i, v) in self.limbs.iter().enumerate() { bounds[i] = bounds[i] .add(&self.bounds[i]) @@ -299,15 +384,18 @@ impl IntVarInner( &self, - other: &IntVarInner, - ) -> Result, SynthesisError> { + other: &LimbedVar, + ) -> Result, SynthesisError> { let len = self.limbs.len() + other.limbs.len() - 1; if self.limbs.is_constant() || other.limbs.is_constant() { // Use the naive approach for constant operands, which costs no @@ -316,7 +404,7 @@ impl IntVarInner>(), @@ -335,14 +423,14 @@ impl IntVarInner IntVarInner( &self, - other: &IntVarInner, + other: &LimbedVar, ) -> Result<(), SynthesisError> { let len = min(self.limbs.len(), other.limbs.len()); // Group the limbs of `self` and `other` so that each group nearly // reaches the capacity `F::MODULUS_MINUS_ONE_DIV_TWO`. // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` - // is the initial number of bits in a limb, just as what we do in grade - // school arithmetic, e.g., + // is `F::BITS_PER_LIMB`, the initial number of bits in a limb. + // This is just as what we do in grade school arithmetic, e.g., // 5 9 // x 7 3 // ------------- @@ -424,9 +512,10 @@ impl IntVarInner 2`. let inv = F::from(BigUint::one() << F::BITS_PER_LIMB) .inverse() .unwrap(); @@ -462,8 +551,8 @@ impl IntVarInner IntVarInner>() .enforce_equal(&FpVar::zero())?; - Bound::add_many(remaining_bounds) + Bounds::add_many(remaining_bounds) .filter_safe::() .ok_or(SynthesisError::Unsatisfiable)?; // For the final carry, we need to ensure that it equals the @@ -508,28 +597,34 @@ impl IntVarInner - IntVarInner + LimbedVar { - /// Convert `Self` to an element in `M`, i.e., compute `Self % M::MODULUS`. - pub fn modulo(&self) -> Result, SynthesisError> { + /// [`LimbedVar::modulo`] computes `self % Target::MODULUS` and returns the + /// result as an aligned [`LimbedVar`]. + /// + /// Note that we allow emulated field elements to be larger than the modulus + /// temporarily during computations, but the final result must be reduced + /// modulo `Target::MODULUS`, and for efficiency, this needs to be done by + /// the caller explicitly. + pub fn modulo(&self) -> Result, SynthesisError> { let cs = self.cs(); let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); // Provide the quotient and remainder as hints - let q = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { + let q = LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); Ok(( compose(self.limbs.value().unwrap_or_default()).div_floor(&m), - Bound(lb, ub), + Bounds(lb, ub), )) })?; - let r = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { + let r = LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { Ok(( compose(self.limbs.value().unwrap_or_default()).abs() % &m, - Bound(Zero::zero(), m.clone()), + Bounds(Zero::zero(), m.clone()), )) })?; - let m = IntVarInner::constant(m); + let m = LimbedVar::constant(m); // Enforce `self = q * m + r` q.mul_unaligned(&m)? @@ -541,23 +636,24 @@ impl Ok(r) } - /// Enforce that `self` is congruent to `other` modulo `M::MODULUS`. + /// [`LimbedVar::enforce_congruent`] enforce that `self` is congruent to + /// `other` modulo `Target::MODULUS`. pub fn enforce_congruent( &self, - other: &IntVarInner, + other: &LimbedVar, ) -> Result<(), SynthesisError> { let cs = self.cs(); let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); // Provide the quotient as hint - let q = IntVarInner::new_variable_with_inferred_mode(cs.clone(), || { + let q = LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); Ok(( compose(self.limbs.value().unwrap_or_default()).div_floor(&m), - Bound(lb, ub), + Bounds(lb, ub), )) })?; - let m = IntVarInner::constant(m); + let m = LimbedVar::constant(m); // Enforce `self - other = q * m` self.sub_unaligned(other)? @@ -565,77 +661,89 @@ impl } } -impl EquivalenceGadget> - for IntVarInner +// The following lines are quite repetitive, but we have to implement them all +// to make the compiler happy. +impl EquivalenceGadget> + for LimbedVar { fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { self.enforce_equal(other) } } -impl EquivalenceGadget> - for IntVarInner +impl EquivalenceGadget> + for LimbedVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + fn enforce_equivalent( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { self.enforce_congruent(other) } } -impl EquivalenceGadget> - for IntVarInner +impl EquivalenceGadget> + for LimbedVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + fn enforce_equivalent( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { self.enforce_congruent(other) } } -impl EquivalenceGadget> - for IntVarInner +impl EquivalenceGadget> + for LimbedVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + fn enforce_equivalent( + &self, + other: &LimbedVar, + ) -> Result<(), SynthesisError> { self.enforce_congruent(other) } } -impl EquivalenceGadget> for IntVarInner { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { self.enforce_equal(other) } } -impl EquivalenceGadget> for IntVarInner { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { self.enforce_equal_unaligned(other) } } -impl EquivalenceGadget> for IntVarInner { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { self.enforce_equal_unaligned(other) } } -impl EquivalenceGadget> for IntVarInner { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { +impl EquivalenceGadget> for LimbedVar { + fn enforce_equivalent(&self, other: &LimbedVar) -> Result<(), SynthesisError> { self.enforce_equal_unaligned(other) } } -impl TryFrom> - for IntVarInner +impl TryFrom> + for LimbedVar { type Error = SynthesisError; - fn try_from(v: IntVarInner) -> Result { + fn try_from(v: LimbedVar) -> Result { v.modulo() } } -impl TwoStageFieldVar for IntVarInner { - type Intermediate = IntVarInner; +impl TwoStageFieldVar for LimbedVar { + type Intermediate = LimbedVar; } -impl EqGadget for IntVarInner { +// Only implement `EqGadget` for aligned variables. +impl EqGadget for LimbedVar { fn is_eq(&self, other: &Self) -> Result, SynthesisError> { let mut result = Boolean::TRUE; if self.limbs.len() != other.limbs.len() { @@ -702,18 +810,28 @@ impl EqGadget for IntVarInner { } } -impl FromBitsGadget for IntVarInner { - fn from_bits_le(bits: &[Boolean], bound: Bound) -> Result { +impl FromBitsGadget for LimbedVar { + fn from_bits_le(bits: &[Boolean]) -> Result { + Self::from_bounded_bits_le( + bits, + Bounds( + BigInt::zero(), + (BigInt::one() << bits.len()) - BigInt::one(), + ), + ) + } + + fn from_bounded_bits_le(bits: &[Boolean], bounds: Bounds) -> Result { Ok(Self::new( bits.chunks(F::BITS_PER_LIMB) .map(Boolean::le_bits_to_fp) .collect::>()?, - compute_bounds(&bound.0, &bound.1, F::BITS_PER_LIMB), + compute_bounds(&bounds.0, &bounds.1, F::BITS_PER_LIMB), )) } } -impl CondSelectGadget for IntVarInner { +impl CondSelectGadget for LimbedVar { fn conditionally_select( cond: &Boolean, true_value: &Self, @@ -742,7 +860,7 @@ impl CondSelectGadget for IntVarInner ToBitsGadget for IntVarInner { +impl ToBitsGadget for LimbedVar { fn to_bits_le(&self) -> Result>, SynthesisError> { for bound in &self.bounds { if bound.0 < BigInt::zero() { @@ -759,7 +877,7 @@ impl ToBitsGadget for IntVarInner { } } -impl AbsorbableGadget for IntVarInner { +impl AbsorbableVar for LimbedVar { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { let bits_per_limb = F::MODULUS_BIT_SIZE as usize - 1; @@ -769,38 +887,13 @@ impl AbsorbableGadget for IntVarInner { } } -impl VectorGadget> - for [IntVarInner] -{ - fn add(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.add_unaligned(y)) - .collect() - } - - fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { - self.iter() - .zip(other.iter()) - .map(|(x, y)| x.mul_unaligned(y)) - .collect() - } - - fn scale( - &self, - other: &IntVarInner, - ) -> Result>, SynthesisError> { - self.iter().map(|x| x.mul_unaligned(other)).collect() - } -} - -impl MatrixGadget> - for SparseMatrixVar> +impl MatrixGadget> + for SparseMatrixVar> { fn mul_vector( &self, - v: &impl Index>, - ) -> Result>, SynthesisError> { + v: &impl Index>, + ) -> Result>, SynthesisError> { self.0 .iter() .map(|row| { @@ -809,13 +902,13 @@ impl MatrixGadget> .map(|(value, col_i)| value.limbs.len() + v[*col_i].limbs.len() - 1) .max() .unwrap_or(0); - // This is a combination of `mul_no_align` and `add_no_align` + // This is a combination of `mul_unaligned` and `add_unaligned` // that results in more flattened `LinearCombination`s. // Consequently, `ConstraintSystem::inline_all_lcs` costs less // time, thus making trusted setup and proof generation faster. let bounds = (0..len) .map(|i| { - Bound::add_many( + Bounds::add_many( &row.iter() .flat_map(|(value, col_i)| { let start = @@ -842,18 +935,18 @@ impl MatrixGadget> .sum() }) .collect(); - Ok(IntVarInner::new(limbs, bounds)) + Ok(LimbedVar::new(limbs, bounds)) }) .collect() } } -pub fn compute_bounds(lb: &BigInt, ub: &BigInt, bits_per_limb: usize) -> Vec { +fn compute_bounds(lb: &BigInt, ub: &BigInt, bits_per_limb: usize) -> Vec { let len = max(lb.bits(), ub.bits()) as usize; let (n_full_limbs, n_remaining_bits) = len.div_rem(&bits_per_limb); let mut bounds = vec![ - Bound( + Bounds( if lb.is_negative() { BigInt::one() - (BigInt::one() << bits_per_limb) } else { @@ -870,21 +963,21 @@ pub fn compute_bounds(lb: &BigInt, ub: &BigInt, bits_per_limb: usize) -> Vec AllocVar<(BigInt, Bound), F> for IntVarInner { - fn new_variable>( +impl AllocVar<(BigInt, Bounds), F> for LimbedVar { + fn new_variable>( cs: impl Into>, f: impl FnOnce() -> Result, mode: AllocationMode, ) -> Result { let cs = cs.into().cs(); let v = f()?; - let (x, Bound(lb, ub)) = v.borrow(); + let (x, Bounds(lb, ub)) = v.borrow(); if x < lb || x > ub { return Err(SynthesisError::Unsatisfiable); @@ -952,9 +1045,9 @@ impl AllocVar<(BigInt, Bound), F> for IntVarInner>, - t: impl Borrow<(BigInt, Bound)>, + t: impl Borrow<(BigInt, Bounds)>, ) -> Result { - let (x, Bound(lb, ub)) = t.borrow(); + let (x, Bounds(lb, ub)) = t.borrow(); if x < lb || x > ub { return Err(SynthesisError::Unsatisfiable); @@ -979,7 +1072,7 @@ impl AllocVar<(BigInt, Bound), F> for IntVarInner, Vec<_>>(); @@ -987,7 +1080,7 @@ impl AllocVar<(BigInt, Bound), F> for IntVarInner AllocVar for IntVarInner { +impl AllocVar for LimbedVar { fn new_variable>( cs: impl Into>, f: impl FnOnce() -> Result, @@ -999,7 +1092,7 @@ impl AllocVar for IntVarInner AllocVar for IntVarInner IntVarInner { +impl LimbedVar { + /// [`LimbedVar::constant`] allocates a constant [`LimbedVar`] with value + /// `x`. pub fn constant(x: BigInt) -> Self { // `unwrap` below is safe because we are allocating a constant value, // which is guaranteed to succeed. - Self::new_constant(ConstraintSystemRef::None, (x.clone(), Bound(x.clone(), x))).unwrap() + Self::new_constant(ConstraintSystemRef::None, (x.clone(), Bounds(x.clone(), x))).unwrap() } } @@ -1091,7 +1186,7 @@ macro_rules! impl_assignment_op { impl_binary_op!( Add, add, - |a: &IntVarInner, b: &IntVarInner| -> IntVarInner { + |a: &LimbedVar, b: &LimbedVar| -> LimbedVar { a.add_unaligned(b).unwrap() }, (F: SonobeField, Cfg, const LHS_ALIGNED: bool, const RHS_ALIGNED: bool), @@ -1100,7 +1195,7 @@ impl_binary_op!( impl_assignment_op!( AddAssign, add_assign, - |a: &mut IntVarInner, b: &IntVarInner| { + |a: &mut LimbedVar, b: &LimbedVar| { *a = a.add_unaligned(b).unwrap() }, (F: SonobeField, Cfg, const ALIGNED: bool), @@ -1109,7 +1204,7 @@ impl_assignment_op!( impl_binary_op!( Sub, sub, - |a: &IntVarInner, b: &IntVarInner| -> IntVarInner { + |a: &LimbedVar, b: &LimbedVar| -> LimbedVar { a.sub_unaligned(b).unwrap() }, (F: SonobeField, Cfg, const SELF_ALIGNED: bool, const OTHER_ALIGNED: bool), @@ -1118,7 +1213,7 @@ impl_binary_op!( impl_assignment_op!( SubAssign, sub_assign, - |a: &mut IntVarInner, b: &IntVarInner| { + |a: &mut LimbedVar, b: &LimbedVar| { *a = a.sub_unaligned(b).unwrap() }, (F: SonobeField, Cfg, const OTHER_ALIGNED: bool), @@ -1127,7 +1222,7 @@ impl_assignment_op!( impl_binary_op!( Mul, mul, - |a: &IntVarInner, b: &IntVarInner| -> IntVarInner { + |a: &LimbedVar, b: &LimbedVar| -> LimbedVar { a.mul_unaligned(b).unwrap() }, (F: SonobeField, Cfg, const SELF_ALIGNED: bool, const OTHER_ALIGNED: bool), @@ -1136,7 +1231,7 @@ impl_binary_op!( impl_assignment_op!( MulAssign, mul_assign, - |a: &mut IntVarInner, b: &IntVarInner| { + |a: &mut LimbedVar, b: &LimbedVar| { *a = a.mul_unaligned(b).unwrap() }, (F: SonobeField, Cfg, const OTHER_ALIGNED: bool), @@ -1192,11 +1287,11 @@ mod tests { for a in v { let cs = ConstraintSystem::::new_ref(); - let a_var = BigIntVar::new_witness(cs.clone(), || { - Ok((a.clone(), Bound(lb.clone(), ub.clone()))) + let a_var = EmulatedIntVar::new_witness(cs.clone(), || { + Ok((a.clone(), Bounds(lb.clone(), ub.clone()))) })?; - let a_const = BigIntVar::::constant(a.clone()); + let a_const = EmulatedIntVar::::constant(a.clone()); assert_eq!(a, a_var.value()?); assert_eq!(a, a_const.value()?); @@ -1220,46 +1315,46 @@ mod tests { let aab = &a * &ab; let abb = &ab * &b; - let a_var = BigIntVar::new_witness(cs.clone(), || { + let a_var = EmulatedIntVar::new_witness(cs.clone(), || { Ok(( a, - Bound( + Bounds( BigInt::one() - (BigInt::one() << size), (BigInt::one() << size) - BigInt::one(), ), )) })?; - let b_var = BigIntVar::new_witness(cs.clone(), || { + let b_var = EmulatedIntVar::new_witness(cs.clone(), || { Ok(( b, - Bound( + Bounds( BigInt::one() - (BigInt::one() << size), (BigInt::one() << size) - BigInt::one(), ), )) })?; - let ab_var = BigIntVar::new_witness(cs.clone(), || { + let ab_var = EmulatedIntVar::new_witness(cs.clone(), || { Ok(( ab, - Bound( + Bounds( BigInt::one() - (BigInt::one() << (size * 2)), (BigInt::one() << (size * 2)) - BigInt::one(), ), )) })?; - let aab_var = BigIntVar::new_witness(cs.clone(), || { + let aab_var = EmulatedIntVar::new_witness(cs.clone(), || { Ok(( aab, - Bound( + Bounds( BigInt::one() - (BigInt::one() << (size * 3)), (BigInt::one() << (size * 3)) - BigInt::one(), ), )) })?; - let abb_var = BigIntVar::new_witness(cs.clone(), || { + let abb_var = EmulatedIntVar::new_witness(cs.clone(), || { Ok(( abb, - Bound( + Bounds( BigInt::one() - (BigInt::one() << (size * 3)), (BigInt::one() << (size * 3)) - BigInt::one(), ), @@ -1340,7 +1435,7 @@ mod tests { let b_var = Vec::>::new_witness(cs.clone(), || Ok(b))?; let c_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(c))?; - let mut r_var: IntVarInner = + let mut r_var: LimbedVar = EmulatedFieldVar::constant(BigUint::zero().into()).into(); for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { r_var = r_var.add_unaligned(&a.mul_unaligned(&b)?)?; diff --git a/crates/primitives/src/algebra/field/mod.rs b/crates/primitives/src/algebra/field/mod.rs index 64b1316b9..8538cae77 100644 --- a/crates/primitives/src/algebra/field/mod.rs +++ b/crates/primitives/src/algebra/field/mod.rs @@ -1,3 +1,6 @@ +//! This module defines extension traits for field elements and their in-circuit +//! counterparts, along with some common implementations. + use ark_ff::{BigInteger, Fp, FpConfig, PrimeField}; use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; use ark_relations::gr1cs::SynthesisError; @@ -10,12 +13,12 @@ use ark_std::{ use crate::{ algebra::{Val, field::emulated::EmulatedFieldVar}, traits::{Inputize, InputizeEmulated}, - transcripts::{Absorbable, AbsorbableGadget}, + transcripts::{Absorbable, AbsorbableVar}, }; pub mod emulated; -/// `SonobeField` trait is a wrapper around `PrimeField` that also includes the +/// [`SonobeField`] trait is a wrapper around [`PrimeField`] that also includes /// necessary bounds for the field to be used conveniently in folding schemes. pub trait SonobeField: PrimeField @@ -23,33 +26,36 @@ pub trait SonobeField: + Inputize + Val, EmulatedVar = EmulatedFieldVar> { + /// [`SonobeField::BITS_PER_LIMB`] defines the bit length of each limb when + /// representing field elements as limbs in an emulated field variable. const BITS_PER_LIMB: usize; } impl, const N: usize> SonobeField for Fp { // For a `F` with order > 250 bits, 55 is chosen for optimizing the most // expensive part `Az∘Bz` when checking the R1CS relation for CycleFold. - // Consider using `NonNativeUintVar` to represent the base field `Fq`. - // Since 250 / 55 = 4.46, the `NonNativeUintVar` has 5 limbs. - // Now, the multiplication of two `NonNativeUintVar`s has 9 limbs, and + // Consider using `EmulatedFieldVar` to represent the base field `Fq`. + // Since 250 / 55 = 4.46, the `EmulatedFieldVar` has 5 limbs. + // Now, the multiplication of two `EmulatedFieldVar`s has 9 limbs, and // each limb has at most 2^{55 * 2} * 5 = 112.3 bits. // For a 1400x1400 matrix `A`, the multiplication of `A`'s row and `z` - // is the sum of 1400 `NonNativeUintVar`s, each with 9 limbs. + // is the sum of 1400 `EmulatedFieldVar`s, each with 9 limbs. // Thus, the maximum bit length of limbs of each element in `Az` is // 2^{55 * 2} * 5 * 1400 = 122.7 bits. // Finally, in the hadamard product of `Az` and `Bz`, every element has // 17 limbs, whose maximum bit length is (2^{55 * 2} * 5 * 1400)^2 * 9 - // = 248.7 bits and is less than the native field `Fr`. + // = 248.7 bits and is less than the constraint field `Fr`. // Thus, 55 allows us to compute `Az∘Bz` without the expensive alignment // operation. // // TODO: either make it a global const, or compute an optimal value // based on the modulus size. - const BITS_PER_LIMB: usize = 55; // TODO: make this configurable + // TODO: make this configurable + const BITS_PER_LIMB: usize = 55; } impl, const N: usize> Val for Fp { - type ConstraintField = Self; + type PreferredConstraintField = Self; type Var = FpVar; type EmulatedVar = EmulatedFieldVar; @@ -78,7 +84,7 @@ impl, const N: usize> Absorbable for Fp { } } -impl AbsorbableGadget for FpVar { +impl AbsorbableVar for FpVar { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { dest.push(self.clone()); Ok(()) @@ -86,16 +92,12 @@ impl AbsorbableGadget for FpVar { } impl, const N: usize> Inputize for Fp { - /// Returns the internal representation in the same order as how the value - /// is allocated in `FpVar::new_input`. fn inputize(&self) -> Vec { vec![*self] } } impl InputizeEmulated for P { - /// Returns the internal representation in the same order as how the value - /// is allocated in `NonNativeUintVar::new_input`. fn inputize_emulated(&self) -> Vec { self.into_bigint() .to_bits_le() @@ -105,6 +107,17 @@ impl InputizeEmulated for P { } } +/// [`TwoStageFieldVar`] abstracts over field variables that support a +/// two-stage arithmetic model. +/// +/// In this model, we consider two stages of in-circuit variables for field +/// elements when performing arithmetic operations: +/// 1. Before the operations, we have the standard field variable type, i.e., +/// the implementor of this trait. +/// 2. During the operations, we use [`TwoStageFieldVar::Intermediate`] to hold +/// the intermediate results. +/// Therefore, the [`Add`] and [`Mul`] operations between two field variables +/// yield an intermediate variable. pub trait TwoStageFieldVar: Clone + Add @@ -112,6 +125,14 @@ pub trait TwoStageFieldVar: + Mul + for<'a> Mul<&'a Self, Output = Self::Intermediate> { + /// The intermediate variable type used during arithmetic operations. + /// + /// We require this type to support conversions from and to the original + /// field variable type. + /// + /// In addition, to allow chaining operations without excessive conversions, + /// we require this type to support [`Add`] and [`Mul`] operations with both + /// itself and the original field variable type. type Intermediate: Clone + From + TryInto @@ -125,6 +146,7 @@ pub trait TwoStageFieldVar: + for<'a> Mul<&'a Self, Output = Self::Intermediate>; } +// Operations over the canonical variable `FpVar` always yield another `FpVar`. impl TwoStageFieldVar for FpVar { type Intermediate = Self; } diff --git a/crates/primitives/src/algebra/group/emulated.rs b/crates/primitives/src/algebra/group/emulated.rs index 477de2b0e..6e6b6f3bb 100644 --- a/crates/primitives/src/algebra/group/emulated.rs +++ b/crates/primitives/src/algebra/group/emulated.rs @@ -1,3 +1,12 @@ +//! This module provides implementation of in-circuit variables for emulated +//! elliptic curve points. +//! +//! This is useful when we want to express points whose coordinates lie in a +//! different field than the circuit's constraint field. +//! +//! Note that currently this module only provides the representation of such +//! points, without any arithmetic operations. + use ark_ec::{AffineRepr, short_weierstrass::SWFlags}; use ark_ff::Zero; use ark_r1cs_std::{ @@ -15,19 +24,20 @@ use ark_std::borrow::Borrow; use crate::{ algebra::{field::emulated::EmulatedFieldVar, group::SonobeCurve}, traits::SonobeField, - transcripts::AbsorbableGadget, + transcripts::AbsorbableVar, }; -/// `EmulatedAffineVar` defines an in-circuit elliptic curve point in its affine -/// representation, where the coordinates are non-native field variables in the -/// curve's base field `Target::BaseField`, emulated over the constraint field -/// `Base`. -/// -/// It is not intended to perform operations, but just to record the coordinates -/// in order to perform hash operations of the point. +/// [`EmulatedAffineVar`] defines an in-circuit elliptic curve point with its +/// affine representation, where the coordinates are in the curve's base field +/// `Target::BaseField` and are emulated over the constraint field `Base` in the +/// circuit. #[derive(Debug, Clone)] pub struct EmulatedAffineVar { + /// [`EmulatedAffineVar::x`] is the x-coordinate of the point's affine + /// representation. pub x: EmulatedFieldVar, + /// [`EmulatedAffineVar::y`] is the y-coordinate of the point's affine + /// representation. pub y: EmulatedFieldVar, } @@ -103,6 +113,8 @@ impl EqGadget for EmulatedAffineVa } impl EmulatedAffineVar { + /// [`EmulatedAffineVar::zero`] allocates the zero point (point at infinity) + /// of the curve as a constant. pub fn zero() -> Self { // `unwrap` below is safe because we are allocating a constant value, // which is guaranteed to succeed. @@ -110,7 +122,7 @@ impl EmulatedAffineVar { } } -impl AbsorbableGadget +impl AbsorbableVar for EmulatedAffineVar { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { @@ -158,20 +170,24 @@ mod tests { } #[test] - fn test_improved_to_hash_preimage() -> Result<(), Box> { + fn test_to_hash_preimage() -> Result<(), Box> { let cs = ConstraintSystem::::new_ref(); - // check that point_to_nonnative_limbs returns the expected values let mut rng = thread_rng(); let p = Projective::rand(&mut rng); let p_var = EmulatedAffineVar::::new_witness(cs.clone(), || Ok(p))?; - assert_eq!(p_var.to_absorbable()?.value()?, p.to_absorbable()); + + let mut v = vec![]; + let mut v_var = vec![]; + p.absorb_into(&mut v); + p_var.absorb_into(&mut v_var)?; + + assert_eq!(v_var.value()?, v); Ok(()) } #[test] fn test_inputize() -> Result<(), Box> { - // check that point_to_nonnative_limbs returns the expected values let mut rng = thread_rng(); let p = Projective::rand(&mut rng); diff --git a/crates/primitives/src/algebra/group/mod.rs b/crates/primitives/src/algebra/group/mod.rs index 3abf989aa..da37399c5 100644 --- a/crates/primitives/src/algebra/group/mod.rs +++ b/crates/primitives/src/algebra/group/mod.rs @@ -1,3 +1,6 @@ +//! This module defines extension traits for elliptic curve points and their +//! in-circuit counterparts, along with some common implementations. + use ark_ec::{ AffineRepr, CurveGroup, PrimeGroup, short_weierstrass::{Projective, SWCurveConfig}, @@ -13,15 +16,17 @@ use ark_relations::gr1cs::SynthesisError; use crate::{ algebra::{Val, field::SonobeField, group::emulated::EmulatedAffineVar}, traits::{Dummy, Inputize, InputizeEmulated}, - transcripts::{Absorbable, AbsorbableGadget}, + transcripts::{Absorbable, AbsorbableVar}, }; pub mod emulated; +/// [`CF1`] is a type alias for the scalar field of a curve `C`. pub type CF1 = ::ScalarField; +/// [`CF2`] is a type alias for the base field of a curve `C`. pub type CF2 = <::BaseField as Field>::BasePrimeField; -/// `SonobeCurve` trait is a wrapper around `CurveGroup` that also includes the +/// [`SonobeCurve`] trait is a wrapper around [`CurveGroup`] that also includes /// necessary bounds for the curve to be used conveniently in folding schemes. pub trait SonobeCurve: CurveGroup @@ -29,7 +34,7 @@ pub trait SonobeCurve: + Inputize + InputizeEmulated + Val< - Var: CurveVar + AbsorbableGadget, + Var: CurveVar + AbsorbableVar, EmulatedVar = EmulatedAffineVar, > { @@ -41,7 +46,7 @@ impl> SonobeC } impl> Val for Projective

{ - type ConstraintField = P::BaseField; + type PreferredConstraintField = P::BaseField; type Var = ProjectiveVar>; type EmulatedVar = EmulatedAffineVar; @@ -61,7 +66,7 @@ impl> Absorbable for Projective

{ } } -impl> AbsorbableGadget +impl> AbsorbableVar for ProjectiveVar> { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { @@ -80,8 +85,6 @@ impl> AbsorbableGadget } impl> Inputize for Projective

{ - /// Returns the internal representation in the same order as how the value - /// is allocated in `ProjectiveVar::new_input`. fn inputize(&self) -> Vec { let affine = self.into_affine(); match affine.xy() { @@ -94,8 +97,6 @@ impl> Inputize for Projec impl> InputizeEmulated for Projective

{ - /// Returns the internal representation in the same order as how the value - /// is allocated in `NonNativeAffineVar::new_input`. fn inputize_emulated(&self) -> Vec { let affine = self.into_affine(); let (x, y) = affine.xy().unwrap_or_default(); @@ -103,69 +104,3 @@ impl> [x, y].inputize_emulated() } } - -// fn lattice_reduction_2x2( -// mut b1: (BigInt, BigInt), -// mut b2: (BigInt, BigInt), -// ) -> ((BigInt, BigInt), (BigInt, BigInt)) { -// loop { -// let mut b1_norm_sq = &b1.0 * &b1.0 + &b1.1 * &b1.1; -// let mut b2_norm_sq = &b2.0 * &b2.0 + &b2.1 * &b2.1; - -// if b1_norm_sq > b2_norm_sq { -// swap(&mut b1, &mut b2); -// swap(&mut b1_norm_sq, &mut b2_norm_sq); -// } - -// let (mut m, r) = (&b1.0 * &b2.0 + &b1.1 * &b2.1).div_rem(&b1_norm_sq); -// if &r + &r >= b1_norm_sq { -// m += BigInt::one(); -// } - -// if m.is_zero() { -// break; -// } - -// b2.0 -= &m * &b1.0; -// b2.1 -= &m * &b1.1; -// } - -// (b1, b2) -// } - -// impl PointScalarMulGadget> for C { -// fn mul_scalar(&self, scalar: &impl ToBitsGadget>) -> Result { -// let scalar = scalar.to_bits_le()?; - -// let cs = scalar.cs(); - -// let m = BigInt::from_biguint(Sign::Plus, CF1::::MODULUS.into()); -// let m_sqrt = m.sqrt(); - -// let (a, b) = lattice_reduction_2x2( -// (m, Zero::zero()), -// ( -// CI2::::from_bits_le(&scalar.value().unwrap_or_default()) -// .into() -// .into(), -// One::one(), -// ), -// ) -// .0; -// let (a_sign, a_abs) = a.into_parts(); -// let (b_sign, b_abs) = b.into_parts(); -// let a_is_negative = -// Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(a_sign == Sign::Minus))?; -// let b_is_negative = -// Boolean::new_variable_with_inferred_mode(cs.clone(), || Ok(b_sign == Sign::Minus))?; - -// // let a = NonNativeUintVar::new_variable_with_inferred_mode(cs.clone(), || { -// // Ok((a_abs.into(), Bound::new_ub(m_sqrt.clone()))) -// // })?; -// // let b = NonNativeUintVar::new_variable_with_inferred_mode(cs, || { -// // Ok((b_abs.into(), Bound::new_ub(m_sqrt))) -// // })?; - -// todo!() -// } -// } diff --git a/crates/primitives/src/algebra/mod.rs b/crates/primitives/src/algebra/mod.rs index 438d26fca..f84a20e46 100644 --- a/crates/primitives/src/algebra/mod.rs +++ b/crates/primitives/src/algebra/mod.rs @@ -1,3 +1,7 @@ +//! This module provides algebraic abstractions used across Sonobe, including +//! field and group type enhancements, in-circuit (both canonical and emulated) +//! variables, and common algebraic operations. + use ark_ff::PrimeField; use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; @@ -7,9 +11,23 @@ pub mod field; pub mod group; pub mod ops; +/// [`Val`] associates a type with its in-circuit variables. pub trait Val { - type ConstraintField: PrimeField; - type Var: AllocVar + GR1CSVar; + /// [`Val::PreferredConstraintField`] is the preferred constraint field for + /// expressing `Self` in-circuit. + type PreferredConstraintField: PrimeField; + + /// [`Val::Var`] is the *canonical* in-circuit variable. + /// + /// In this case, the circuit is defined over the preferred constraint field + /// and can represent `Self` directly (i.e., without emulation). + type Var: AllocVar + + GR1CSVar; + /// [`Val::EmulatedVar`] is the *emulated* in-circuit variable. + /// + /// In this case, the circuit is defined over an arbitrary field `F` which + /// may differ from the preferred constraint field, and `Self` is + /// represented in-circuit via emulation. type EmulatedVar: AllocVar + GR1CSVar; } diff --git a/crates/primitives/src/algebra/ops/bits.rs b/crates/primitives/src/algebra/ops/bits.rs index e438db2c0..eb997e259 100644 --- a/crates/primitives/src/algebra/ops/bits.rs +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -1,10 +1,15 @@ +//! This module defines traits for conversion between bit representations and +//! algebraic types inside and outside circuits. + use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, eq::EqGadget, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; -use crate::algebra::field::emulated::Bound; +use crate::algebra::field::emulated::Bounds; +/// [`FromBits`] reconstructs a value from bits. pub trait FromBits { + /// [`FromBits::from_bits_le`] computes a value from its little-endian bits. fn from_bits_le(bits: &[bool]) -> Self; } @@ -14,22 +19,45 @@ impl FromBits for F { } } +/// [`FromBitsGadget`] is the in-circuit counterpart of [`FromBits`], which +/// reconstructs an in-circuit variable from boolean variables. pub trait FromBitsGadget: Sized { - fn from_bits_le(bits: &[Boolean], bound: Bound) -> Result; + /// [`FromBitsGadget::from_bits_le`] computes a variable from its + /// little-endian bits, inferring bounds from the length of `bits`. + fn from_bits_le(bits: &[Boolean]) -> Result; + + /// [`FromBitsGadget::from_bounded_bits_le`] computes a variable from its + /// little-endian bits with explicitly supplied [`Bounds`]. + fn from_bounded_bits_le(bits: &[Boolean], bounds: Bounds) -> Result; } +/// [`ToBitsGadgetExt`] extends the standard [`ark_r1cs_std::convert::ToBitsGadget`] +/// with more functionality. pub trait ToBitsGadgetExt: Sized { + /// [`ToBitsGadgetExt::to_n_bits_le`] decomposes `self` into `n` + /// little-endian bits. + /// + /// An error is returned if `self` cannot be represented in `n` bits. fn to_n_bits_le(&self, n: usize) -> Result>, SynthesisError>; + /// [`ToBitsGadgetExt::enforce_bit_length`] enforces that `self` can be + /// represented in at most `n` bits. + /// + /// This is useful for checking that a field element is within the range of + /// `[0, 2^n - 1]` fn enforce_bit_length(&self, n: usize) -> Result<(), SynthesisError> { self.to_n_bits_le(n)?; Ok(()) } } impl FromBitsGadget for FpVar { - fn from_bits_le(bits: &[Boolean], _bound: Bound) -> Result { + fn from_bits_le(bits: &[Boolean]) -> Result { Boolean::le_bits_to_fp(bits) } + + fn from_bounded_bits_le(bits: &[Boolean], _bounds: Bounds) -> Result { + Self::from_bits_le(bits) + } } impl ToBitsGadgetExt for FpVar { diff --git a/crates/primitives/src/algebra/ops/eq.rs b/crates/primitives/src/algebra/ops/eq.rs index 67be1e997..294ba63aa 100644 --- a/crates/primitives/src/algebra/ops/eq.rs +++ b/crates/primitives/src/algebra/ops/eq.rs @@ -1,24 +1,30 @@ +//! This module defines traits for enforcing custom, user-defined equivalence +//! relation between in-circuit variables, enabling flexible checks for equality +//! and congruence. + use ark_ff::PrimeField; use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; -/// `EquivalenceGadget` enforces that two in-circuit variables are "equivalent". +/// [`EquivalenceGadget`] enforces two in-circuit variables are "equivalent". /// /// This does not only allow us to ensure the equality of two variables of the /// same type, but can also be used for guaranteeing variables of different /// types represent the "same" (depending on the context) value. pub trait EquivalenceGadget { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError>; + /// [`EquivalenceGadget::enforce_equivalent`] enforces that `self` and + /// `other` are equivalent. + fn enforce_equivalent(&self, other: &Other) -> Result<(), SynthesisError>; } impl EquivalenceGadget> for FpVar { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + fn enforce_equivalent(&self, other: &FpVar) -> Result<(), SynthesisError> { self.enforce_equal(other) } } impl> EquivalenceGadget<[T]> for [T] { - fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + fn enforce_equivalent(&self, other: &[T]) -> Result<(), SynthesisError> { self.iter() .zip(other) .try_for_each(|(a, b)| a.enforce_equivalent(b)) diff --git a/crates/primitives/src/algebra/ops/matrix.rs b/crates/primitives/src/algebra/ops/matrix.rs index e1347f30e..fef5a7d04 100644 --- a/crates/primitives/src/algebra/ops/matrix.rs +++ b/crates/primitives/src/algebra/ops/matrix.rs @@ -1,3 +1,6 @@ +//! This module defines in-circuit sparse matrix types and implements operations +//! over them. + use ark_ff::PrimeField; use ark_r1cs_std::{ GR1CSVar, @@ -7,11 +10,17 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{Matrix, Namespace, SynthesisError}; use ark_std::{borrow::Borrow, ops::Index}; +/// [`MatrixGadget`] defines operations on in-circuit matrix variables. pub trait MatrixGadget { + /// [`MatrixGadget::mul_vector`] computes the product of `self` and a column + /// vector `v`. fn mul_vector(&self, v: &impl Index) -> Result, SynthesisError>; } -// same format as the native SparseMatrix (which follows ark_relations::gr1cs::Matrix format) +/// [`SparseMatrixVar`] is a sparse matrix represented as a vector of rows, +/// where each row is a vector of `(value, column_index)` pairs. +/// +/// This follows the same format as [`ark_relations::gr1cs::Matrix`]. #[derive(Debug, Clone)] pub struct SparseMatrixVar(pub Vec>); @@ -51,6 +60,15 @@ impl MatrixGadget> for SparseMatrixVar> { .0 .iter() .map(|row| { + // Theoretically we can use `Iterator::sum` directly: + // ```rs + // row + // .iter() + // .map(|(value, col_i)| value * &v[*col_i]) + // .sum() + // ``` + // But it seems that arkworks will throw an error if we do so + // when the products are all constant values... let products = row .iter() .map(|(value, col_i)| value * &v[*col_i]) diff --git a/crates/primitives/src/algebra/ops/mod.rs b/crates/primitives/src/algebra/ops/mod.rs index bbd850498..50c0595c8 100644 --- a/crates/primitives/src/algebra/ops/mod.rs +++ b/crates/primitives/src/algebra/ops/mod.rs @@ -1,3 +1,14 @@ +//! This module collects common algebraic operation traits and their in-circuit +//! gadgets, including: +//! +//! * [`bits`]: conversions between bit representations and algebraic variables. +//! * [`eq`]: generalization of equality checks. +//! * [`matrix`]: sparse matrix representation and operations. +//! * [`poly`]: helpers for polynomial operations. +//! * [`pow`]: computation of powers. +//! * [`rlc`]: random linear combinations. +//! * [`vector`]: vector operations. + pub mod bits; pub mod eq; pub mod matrix; diff --git a/crates/primitives/src/algebra/ops/poly.rs b/crates/primitives/src/algebra/ops/poly.rs index 91696240a..fed1992fd 100644 --- a/crates/primitives/src/algebra/ops/poly.rs +++ b/crates/primitives/src/algebra/ops/poly.rs @@ -1,3 +1,5 @@ +//! This module provides helpers for working with polynomials inside circuits. + use ark_ff::{Field, PrimeField, Zero}; use ark_poly::{DenseMultilinearExtension, EvaluationDomain, GeneralEvaluationDomain}; use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; @@ -6,7 +8,11 @@ use ark_std::log2; use super::pow::Pow; +/// [`MLEHelper`] provides functionality for multilinear extensions. pub trait MLEHelper { + /// [`MLEHelper::from_evaluations`] builds a multilinear extension from a + /// (possibly non-power-of-two) vector of evaluations, padding with zeros + /// up to the next power of two. fn from_evaluations(evaluations: &[F]) -> Self; } @@ -18,14 +24,23 @@ impl MLEHelper for DenseMultilinearExtension { } } +/// [`EvaluationDomainGadget`] provides a subset of evaluation domain operations +/// in [`EvaluationDomain`] for in-circuit field variables. pub trait EvaluationDomainGadget { + /// [`EvaluationDomainGadget::evaluate_all_lagrange_coefficients_var`] + /// computes all Lagrange basis polynomials evaluated at `tau`. + /// + /// It is the in-circuit counterpart of [`EvaluationDomain::evaluate_all_lagrange_coefficients`]. fn evaluate_all_lagrange_coefficients_var( &self, tau: &FpVar, ) -> Result>, SynthesisError>; - fn evaluate_vanishing_polynomial_var(&self, tau: &FpVar) - -> Result, SynthesisError>; + /// [`EvaluationDomainGadget::evaluate_vanishing_polynomial_var`] evaluates + /// the vanishing polynomial of the domain at `tau`. + /// + /// It is the in-circuit counterpart of [`EvaluationDomain::evaluate_vanishing_polynomial`]. + fn evaluate_vanishing_polynomial_var(&self, tau: &FpVar) -> Result, SynthesisError>; } impl EvaluationDomainGadget for GeneralEvaluationDomain { @@ -39,6 +54,8 @@ impl EvaluationDomainGadget for GeneralEvaluationDomain { let offset_inv = self.coset_offset_inv(); let group_gen = self.group_gen(); + // We assume that the evaluation of vanishing polynomial at tau is non-0 + let l_i = (tau.pow_by_constant([size])? * offset_inv.pow([size - 1]) - offset) * size_inv; group_gen diff --git a/crates/primitives/src/algebra/ops/pow.rs b/crates/primitives/src/algebra/ops/pow.rs index ac78ba6a4..3d1808522 100644 --- a/crates/primitives/src/algebra/ops/pow.rs +++ b/crates/primitives/src/algebra/ops/pow.rs @@ -1,14 +1,21 @@ +//! This module defines and implements powering utilities in and out of circuit. + use ark_ff::{Field, PrimeField}; use ark_r1cs_std::fields::{FieldVar, fp::FpVar}; +/// [`Pow`] provides powering operations for field elements. pub trait Pow: Sized { - /// Compute `self^0, self^1, ..., self^{n-1}` + /// [`Pow::powers`] computes: + /// $self^0, self^1, ..., self^{n-1}$. fn powers(&self, n: usize) -> Vec; - /// Compute `self^{2^0}, self^{2^1}, ..., self^{2^{n-1}}` + /// [`Pow::repeated_squares`] computes: + /// $self^{2^0}, self^{2^1}, ..., self^{2^{n-1}}$. fn repeated_squares(&self, n: usize) -> Vec; - /// Compute `self^0, self^1, ..., self^{2^n - 1}` from repeated squares + /// [`Pow::powers_from_repeated_squares`] expands a vector of repeated + /// squares $x^{2^0}, x^{2^1}, ..., x^{2^{n-1}}$ into all powers: + /// $x^0, x^1, ..., x^{2^n - 1}$. fn powers_from_repeated_squares(squares: &[Self]) -> Vec; } @@ -42,11 +49,20 @@ impl Pow for F { } } +/// [`PowGadget`] is the in-circuit counterpart of [`Pow`], providing powering +/// operations for field element variables. pub trait PowGadget: Sized { + /// [`PowGadget::powers`] computes: + /// $self^0, self^1, ..., self^{n-1}$. fn powers(&self, n: usize) -> Vec; + /// [`PowGadget::repeated_squares`] computes: + /// $self^{2^0}, self^{2^1}, ..., self^{2^{n-1}}$. fn repeated_squares(&self, n: usize) -> Vec; + /// [`PowGadget::powers_from_repeated_squares`] expands a vector of repeated + /// squares $x^{2^0}, x^{2^1}, ..., x^{2^{n-1}}$ into all powers: + /// $x^0, x^1, ..., x^{2^n - 1}$. fn powers_from_repeated_squares(squares: &[Self]) -> Vec; } diff --git a/crates/primitives/src/algebra/ops/rlc.rs b/crates/primitives/src/algebra/ops/rlc.rs index 3e43ade12..b25a23d70 100644 --- a/crates/primitives/src/algebra/ops/rlc.rs +++ b/crates/primitives/src/algebra/ops/rlc.rs @@ -1,11 +1,23 @@ +//! This module defines and implements the computation of random linear +//! combination (RLC). +//! +//! An RLC computes $\sum v_i \cdot c_i$ where $v_i$ are values (scalars or +//! vectors) and $c_i$ are the randomness (challenge coefficients), which is +//! used extensively in folding schemes. + use ark_std::{ iter::Sum, ops::{Add, Mul}, }; +/// [`ScalarRLC`] computes the random linear combination for a sequence of +/// scalars (i.e., each $v_i$ is a scalar). pub trait ScalarRLC { + /// [`ScalarRLC::Value`] is the result type of the RLC computation. type Value; + /// [`ScalarRLC::scalar_rlc`] evaluates the RLC with the given coefficients + /// `coeffs`. fn scalar_rlc(self, coeffs: &[Coeff]) -> Self::Value; } @@ -20,9 +32,15 @@ where } } +/// [`SliceRLC`] computes the random linear combination for a sequence of +/// vectors (i.e., each $v_i$ is a vector), by computing the RLC element-wise. +// TODO (@winderica): can we unify `ScalarRLC` and `SliceRLC` into one trait? pub trait SliceRLC { + /// [`SliceRLC::Value`] is the result type of the RLC computation. type Value; + /// [`SliceRLC::slice_rlc`] evaluates the RLC with the given coefficients + /// `coeffs`. fn slice_rlc(self, coeffs: &[Coeff]) -> Vec; } diff --git a/crates/primitives/src/algebra/ops/vector.rs b/crates/primitives/src/algebra/ops/vector.rs index 8a7019a6d..cfcdc6fb5 100644 --- a/crates/primitives/src/algebra/ops/vector.rs +++ b/crates/primitives/src/algebra/ops/vector.rs @@ -1,28 +1,54 @@ -use ark_ff::PrimeField; -use ark_r1cs_std::fields::fp::FpVar; +//! This module provides definitions and implementations of in-circuit vector +//! operations. + use ark_relations::gr1cs::SynthesisError; +use ark_std::ops::{Add, Mul, Sub}; +/// [`VectorGadget`] defines operations on in-circuit vector variables. pub trait VectorGadget { + /// [`VectorGadget::add`] computes the element-wise sum of two vectors. fn add(&self, other: &Self) -> Result, SynthesisError>; - fn scale(&self, scalar: &FV) -> Result, SynthesisError>; + /// [`VectorGadget::sub`] computes the element-wise difference of two + /// vectors. + fn sub(&self, other: &Self) -> Result, SynthesisError>; + + /// [`VectorGadget::scale`] multiplies every element by a scalar. + fn scale(&self, scalar: &Scalar) -> Result, SynthesisError> + where + for<'a> &'a Scalar: Mul<&'a FV, Output = Output>; + /// [`VectorGadget::hadamard`] computes the element-wise (Hadamard) product + /// of two vectors. fn hadamard(&self, other: &Self) -> Result, SynthesisError>; } -impl VectorGadget> for [FpVar] { - fn add(&self, other: &Self) -> Result>, SynthesisError> { +impl VectorGadget for [FV] +where + for<'a> &'a FV: Add<&'a FV, Output = FV> + Sub<&'a FV, Output = FV> + Mul<&'a FV, Output = FV>, +{ + fn add(&self, other: &Self) -> Result, SynthesisError> { if self.len() != other.len() { return Err(SynthesisError::Unsatisfiable); } Ok(self.iter().zip(other.iter()).map(|(a, b)| a + b).collect()) } - fn scale(&self, scalar: &FpVar) -> Result>, SynthesisError> { - Ok(self.iter().map(|a| a * scalar).collect()) + fn sub(&self, other: &Self) -> Result, SynthesisError> { + if self.len() != other.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(self.iter().zip(other.iter()).map(|(a, b)| a - b).collect()) + } + + fn scale(&self, scalar: &Scalar) -> Result, SynthesisError> + where + for<'a> &'a Scalar: Mul<&'a FV, Output = Output>, + { + Ok(self.iter().map(|a| scalar * a).collect()) } - fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { + fn hadamard(&self, other: &Self) -> Result, SynthesisError> { if self.len() != other.len() { return Err(SynthesisError::Unsatisfiable); } diff --git a/crates/primitives/src/arithmetizations/ccs/circuits.rs b/crates/primitives/src/arithmetizations/ccs/circuits.rs index f9917cb35..fe9011ae7 100644 --- a/crates/primitives/src/arithmetizations/ccs/circuits.rs +++ b/crates/primitives/src/arithmetizations/ccs/circuits.rs @@ -1,3 +1,5 @@ +//! This module implements in-circuit CCS variables. + use ark_ff::PrimeField; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, @@ -9,12 +11,15 @@ use ark_std::borrow::Borrow; use super::{CCS, CCSVariant}; use crate::algebra::ops::matrix::SparseMatrixVar; -/// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. +/// [`CCSMatricesVar`] is an in-circuit variable of a given CCS structure. +/// +/// Only the matrices are represented, while the remaining CCS parameters are +/// constants to the circuit. #[allow(non_snake_case)] #[derive(Debug, Clone)] pub struct CCSMatricesVar { - // we only need native representation, so the constraint field==F - pub M: Vec>>, + #[allow(dead_code)] + M: Vec>>, } impl AllocVar, F> for CCSMatricesVar { @@ -36,3 +41,5 @@ impl AllocVar, F> for CCSMatricesVar }) } } + +// TODO: add relation check gadgets when needed. diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 722581134..9ef789298 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -1,3 +1,24 @@ +//! This module implements the Customizable Constraint System (CCS) and its +//! relation checks against plain witnesses and instances. +//! +//! Proposed in the CCS [paper], it is a generalization of R1CS as well as many +//! other constraint systems. +//! A CCS structure is defined by the following components: +//! - The number of constraints `m`, the number of variables `n`, and the number +//! of public inputs `l`. +//! - The degree `d`. +//! - A sequence of `t` matrices `M`. +//! - A sequence of `q` multisets `S`, where each multiset `S_i` has at most `d` +//! elements and each element is an index in `[0, t - 1]` pointing to a matrix +//! `M_j`. +//! - A sequence of `q` coefficients `c`. +//! +//! A vector of assignments `z` satisfies the CCS if its evaluation +//! `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j · z))` is zero, where `〇` denotes +//! the Hadamard product among all `M_j · z`. +//! +//! [paper]: https://eprint.iacr.org/2023/552.pdf + use ark_ff::Field; use ark_poly::DenseMultilinearExtension; use ark_relations::gr1cs::{ConstraintSystem, Matrix}; @@ -14,16 +35,26 @@ use crate::{ pub mod circuits; +/// [`CCSVariant`] defines the methods that a CCS variant (e.g., R1CS) should +/// implement. pub trait CCSVariant: Clone + Debug + PartialEq + Default + Sync { + /// [`CCSVariant::n_matrices`] returns the number of matrices in the CCS + /// variant. fn n_matrices() -> usize; + /// [`CCSVariant::degree`] returns the degree of the CCS variant. fn degree() -> usize; + /// [`CCSVariant::multisets_vec`] returns the vector of multisets in the CCS + /// variant. fn multisets_vec() -> Vec>; + /// [`CCSVariant::coefficients_vec`] returns the vector of coefficients in + /// the CCS variant. fn coefficients_vec() -> Vec; } +/// [`CCSConfig`] stores the shape parameters of a CCS structure. #[allow(non_snake_case)] #[derive(Clone, Debug, Default, PartialEq)] pub struct CCSConfig { @@ -61,11 +92,6 @@ impl ArithConfig for CCSConfig { fn n_witnesses(&self) -> usize { self.n_variables() - self.n_public_inputs() - 1 } - - #[inline] - fn set_n_public_inputs(&mut self, l: usize) { - self.l = l; - } } impl, V: CCSVariant> From for CCSConfig { @@ -86,42 +112,40 @@ impl From<&ConstraintSystem> for CCSConfig { } } -/// CCS represents the Customizable Constraint Systems structure defined in -/// the [CCS paper](https://eprint.iacr.org/2023/552) +/// [`CCS`] holds the CCS matrices `M` together with the configuration. #[allow(non_snake_case)] #[derive(Clone)] pub struct CCS { cfg: CCSConfig, - /// vector of matrices - pub M: Vec>, + pub(super) M: Vec>, } impl CCS { - /// Evaluates the CCS relation at a given vector of assignments `z` - pub fn eval_assignments( - &self, - z: Assignments + Sync>, - ) -> Result, Error> { + /// [`CCS::evaluate_at`] evaluates the CCS relation at a given vector of + /// assignments `z`. + pub fn evaluate_at(&self, z: Assignments + Sync>) -> Result, Error> { + let cfg = &self.cfg; + let public_len = z.public.as_ref().len(); let private_len = z.private.as_ref().len(); - if public_len != self.n_public_inputs() { + if public_len != cfg.n_public_inputs() { return Err(Error::MalformedAssignments(format!( "The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", - self.n_public_inputs(), + cfg.n_public_inputs(), public_len ))); } - if private_len != self.n_witnesses() { + if private_len != cfg.n_witnesses() { return Err(Error::MalformedAssignments(format!( "The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", - self.n_witnesses(), + cfg.n_witnesses(), private_len ))); } // Recall that the evaluation of CCS at z is defined as: - // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i * z))$, + // `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j · z))`, // where $\prod$ denotes the Hadamard product. // // Below, we manually expand the vector and matrix operations for less @@ -131,21 +155,21 @@ impl CCS { // We parallelize the outer loop over rows (when the `parallel` feature // is enabled), since the number of constraints in the CCS is typically // large in practice. - Ok(cfg_into_iter!(0..self.n_constraints()) + Ok(cfg_into_iter!(0..cfg.n_constraints()) .map(|row| { - // The row-th entry of the resulting vector is: - // $\sum_{j=0}^{q - 1} (c_j * \prod_{i \in S_j} (M_i[row] * z))$ + // The `row`-th entry of the resulting vector is: + // `Σ_{i ∈ {0, q-1}} (c_i · 〇_{j ∈ S_i} (M_j[row] · z))` V::multisets_vec() .into_iter() .zip(V::coefficients_vec::()) .map(|(s, c)| { // Each term in the sum is: - // $c_j * \prod_{i \in S_j} (M_i[row] * z)$ + // `c_i · 〇_{j ∈ S_i} (M_j[row] · z)` c * s .iter() .map(|&i| { - // Each factor in the product is $M_i[row] * z$, - // i.e., the dot product of $M_i[row]$ and $z$. + // Each factor in the product is `M_j[row] · z`, + // i.e., the dot product of `M_j[row]` and `z`. self.M[i][row] .iter() .map(|(val, col)| z[*col] * val) @@ -158,6 +182,8 @@ impl CCS { .collect()) } + /// [`CCS::mles`] returns the multilinear extensions of all CCS matrices + /// `M_i` evaluated over the assignments `z`. pub fn mles( &self, z: Assignments + Sync>, @@ -202,7 +228,7 @@ impl, U: AsRef<[F]>, V: CCSVariant> ArithRelation type Evaluation = Vec; fn eval_relation(&self, w: &W, u: &U) -> Result { - self.eval_assignments((F::one(), u.as_ref(), w.as_ref()).into()) + self.evaluate_at((F::one(), u.as_ref(), w.as_ref()).into()) } fn check_evaluation(_w: &W, _u: &U, e: Self::Evaluation) -> Result<(), Error> { diff --git a/crates/primitives/src/arithmetizations/mod.rs b/crates/primitives/src/arithmetizations/mod.rs index a9dd944ae..6fc4170e8 100644 --- a/crates/primitives/src/arithmetizations/mod.rs +++ b/crates/primitives/src/arithmetizations/mod.rs @@ -1,3 +1,11 @@ +//! This module defines and implements traits for arithmetizations, also known +//! as constraint systems. +//! +//! In Sonobe, we currently support two constraint systems: the Rank-1 +//! Constraint System (R1CS) and the Customizable Constraint System (CCS). +//! However, user circuits are always synthesized into R1CS currently, since +//! R1CS is the only supported constraint system by ark-relations. + use ark_relations::gr1cs::SynthesisError; use ark_std::{fmt::Debug, log2}; use thiserror::Error; @@ -7,190 +15,173 @@ use crate::relations::{Relation, RelationGadget}; pub mod ccs; pub mod r1cs; -#[derive(Debug, Error)] +/// [`Error`] enumerates possible errors during arithmetization operations. +#[derive(Error, Debug)] pub enum Error { + /// [`Error::MalformedAssignments`] indicates that the provided assignments + /// have incorrect shape. #[error("The provided assignments have incorrect shape: {0}")] MalformedAssignments(String), + /// [`Error::UnsatisfiedAssignments`] indicates that the provided + /// assignments do not satisfy the constraint system. #[error("The provided assignments do not satisfy the constraint system: {0}")] UnsatisfiedAssignments(String), - #[error("Failed to extract constraints from the constraint system: {0}")] - ConstraintExtractionFailure(String), + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. #[error(transparent)] SynthesisError(#[from] SynthesisError), } +/// [`ArithConfig`] describes the configuration of a constraint system. pub trait ArithConfig: Clone + Debug + Default + PartialEq { - /// Returns the degree of the constraint system + /// [`ArithConfig::degree`] returns the degree of the constraint system. fn degree(&self) -> usize; - /// Returns the number of constraints in the constraint system + /// [`ArithConfig::n_constraints`] returns the number of constraints in the + /// constraint system. fn n_constraints(&self) -> usize; + /// [`ArithConfig::log_constraints`] returns the base-2 logarithm of the + /// number of constraints in the constraint system. fn log_constraints(&self) -> usize { log2(self.n_constraints()) as usize } - /// Returns the number of variables in the constraint system + /// [`ArithConfig::n_variables`] returns the number of variables in the + /// constraint system. fn n_variables(&self) -> usize; - /// Returns the number of public inputs / public IO / instances / statements - /// in the constraint system + /// [`ArithConfig::n_public_inputs`] returns the number of public inputs in + /// the constraint system. fn n_public_inputs(&self) -> usize; - /// Returns the number of witnesses / secret inputs in the constraint system + /// [`ArithConfig::n_witnesses`] returns the number of witnesses in the + /// constraint system. fn n_witnesses(&self) -> usize; - - fn set_n_public_inputs(&mut self, l: usize); } -/// [`Arith`] is a trait about constraint systems (R1CS, CCS, etc.), where we -/// define methods for getting information about the constraint system. +/// [`Arith`] is a trait for constraint systems (R1CS, CCS, etc.), where we +/// define methods to get and set configuration about the constraint system. +/// In addition to the configuration, the implementor of this trait may also +/// store the actual constraints and other information. pub trait Arith: Clone + Default { + /// [`Arith::Config`] specifies the arithmetization's configuration. type Config: ArithConfig; + /// [`Arith::config`] returns a reference to the configuration of the + /// constraint system. fn config(&self) -> &Self::Config; + /// [`Arith::config_mut`] returns a mutable reference to the configuration + /// of the constraint system. fn config_mut(&mut self) -> &mut Self::Config; - - /// Returns the degree of the constraint system - #[inline] - fn degree(&self) -> usize { - self.config().degree() - } - - /// Returns the number of constraints in the constraint system - #[inline] - fn n_constraints(&self) -> usize { - self.config().n_constraints() - } - - #[inline] - fn log_constraints(&self) -> usize { - self.config().log_constraints() - } - - /// Returns the number of variables in the constraint system - #[inline] - fn n_variables(&self) -> usize { - self.config().n_variables() - } - - /// Returns the number of public inputs / public IO / instances / statements - /// in the constraint system - #[inline] - fn n_public_inputs(&self) -> usize { - self.config().n_public_inputs() - } - - /// Returns the number of witnesses / secret inputs in the constraint system - #[inline] - fn n_witnesses(&self) -> usize { - self.config().n_witnesses() - } } -/// `ArithRelation` *treats a constraint system as a relation* between a witness -/// of type `W` and a statement / public input / public IO / instance of type -/// `U`, and in this trait, we define the necessary operations on the relation. +/// [`ArithRelation`] treats a constraint system as a relation between a witness +/// of type `W` and an instance of type `U`, and in this trait, we separate the +/// relation check into two steps: evaluating the constraint system and checking +/// the evaluation result. /// -/// Note that the same constraint system may support different types of `W` and -/// `U`, and the satisfiability check may vary. +/// Note that `W` and `U` are part of the trait parameters instead of associated +/// types, because the same constraint system may support different types of `W` +/// and `U`, and the satisfiability check may vary. +/// This "same constraint system, different witness-instance pair" abstraction +/// turns out to be very flexible, as one constraint system struct now can have +/// many different relation checks depending on the context. /// -/// For example, both plain R1CS and relaxed R1CS are represented by 3 matrices, -/// but the types of `W` and `U` are different: -/// - The plain R1CS has `W` and `U` as vectors of field elements. -/// -/// `W = w` and `U = x` satisfy R1CS if `Az ∘ Bz = Cz`, where `z = [1, x, w]`. +/// For example, some folding schemes consider a variant of R1CS known as +/// relaxed R1CS, which is also represented by the `A`, `B`, and `C` matrices +/// but has a different relation check compared to plain R1CS. +/// We handle their similarities and differences in the following way: +/// - Since the structure of relaxed R1CS is exactly the same as plain R1CS, we +/// use a single R1CS struct to represent both of them. +/// - To distinguish their relation checks, we instead use distinct types of `W` +/// and `U`. +/// - For plain R1CS, we use plain witness `W = w` and instance `U = x` that +/// are simply vectors of field elements. +/// The implementation of `ArithRelation` for such `W` and `U` then checks +/// if `Az ∘ Bz = Cz`, where `z = [1, x, w]`. +/// - For relaxed R1CS, we use relaxed witness `W` and relaxed instance `U` +/// that contain extra data such as the error or slack terms, e.g., +/// - In Nova, `W = (w, e, ...)`, `U = (u, x, ...)`. +/// The implementation of `ArithRelation` for such `W` and `U` checks +/// if `Az ∘ Bz = uCz + e`, where `z = [u, x, w]`. +/// - In ProtoGalaxy, `W = (w, ...)`, `U = (x, e, β, ...)`. +/// The implementation of `ArithRelation` for such `W` and `U` checks +/// if `e = Σ pow_i(β) v_i`, where `v = Az ∘ Bz - Cz`,`z = [1, x, w]`. /// -/// - In Nova, Relaxed R1CS has `W` as [`crate::folding::nova::Witness`], -/// and `U` as [`crate::folding::nova::CommittedInstance`]. -/// -/// `W = (w, e, ...)` and `U = (u, x, ...)` satisfy Relaxed R1CS if -/// `Az ∘ Bz = uCz + e`, where `z = [u, x, w]`. -/// (commitments in `U` are not checked here) -/// -/// Also, `W` and `U` have non-native field elements as their components when -/// used as CycleFold witness and instance. -/// -/// - In ProtoGalaxy, Relaxed R1CS has `W` as [`crate::folding::protogalaxy::Witness`], -/// and `U` as [`crate::folding::protogalaxy::CommittedInstance`]. -/// -/// `W = (w, ...)` and `U = (x, e, β, ...)` satisfy Relaxed R1CS if -/// `e = Σ pow_i(β) v_i`, where `v = Az ∘ Bz - Cz`, `z = [1, x, w]`. -/// (commitments in `U` are not checked here) -/// -/// This is also the case of CCS, where `W` and `U` may be vectors of field -/// elements, [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::lcccs::LCCCS`], -/// or [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::cccs::CCCS`]. +/// This is also the case for CCS, where `W` and `U` may be vectors of field +/// elements or running / incoming witness-instance pairs of different folding +/// schemes such as HyperNova. pub trait ArithRelation: Arith { + /// [`ArithRelation::Evaluation`] defines the type of the evaluation result + /// returned by [`ArithRelation::eval_relation`], and consumed by + /// [`ArithRelation::check_evaluation`]. + /// + /// The evaluation result is usually a vector of field elements. + /// However, we use an associated type to represent the evaluation result + /// for future extensions. type Evaluation; - /// Evaluates the constraint system `self` at witness `w` and instance `u`. - /// Returns the evaluation result. + /// [`ArithRelation::eval_relation`] evaluates the constraint system at + /// witness `w` and instance `u`. It returns the evaluation result. /// - /// The evaluation result is usually a vector of field elements. /// For instance: /// - Evaluating the plain R1CS at `W = w` and `U = x` returns /// `Az ∘ Bz - Cz`, where `z = [1, x, w]`. - /// /// - Evaluating the relaxed R1CS in Nova at `W = (w, e, ...)` and /// `U = (u, x, ...)` returns `Az ∘ Bz - uCz`, where `z = [u, x, w]`. - /// /// - Evaluating the relaxed R1CS in ProtoGalaxy at `W = (w, ...)` and /// `U = (x, e, β, ...)` returns `Az ∘ Bz - Cz`, where `z = [1, x, w]`. - /// - /// However, we use `Self::Evaluation` to represent the evaluation result - /// for future extensibility. fn eval_relation(&self, w: &W, u: &U) -> Result; - /// Checks if the evaluation result is valid. The witness `w` and instance - /// `u` are also parameters, because the validity check may need information - /// contained in `w` and/or `u`. + /// [`ArithRelation::check_evaluation`] checks if the evaluation result is + /// valid. The witness `w` and instance `u` are also parameters, because the + /// validity check may need information contained in `w` and/or `u`. /// /// For instance: /// - The evaluation `v` of plain R1CS at satisfying `W` and `U` should be /// an all-zero vector. - /// /// - The evaluation `v` of relaxed R1CS in Nova at satisfying `W` and `U` - /// should be equal to the error term `e` in the witness. - /// + /// should be equal to the error term `e` in `W`. /// - The evaluation `v` of relaxed R1CS in ProtoGalaxy at satisfying `W` /// and `U` should satisfy `e = Σ pow_i(β) v_i`, where `e` is the error - /// term in the committed instance. + /// term in `U`. fn check_evaluation(w: &W, u: &U, v: Self::Evaluation) -> Result<(), Error>; } impl> Relation for A { type Error = Error; - /// Checks if witness `w` and instance `u` satisfy the constraint system - /// `self` by first computing the evaluation result and then checking the - /// validity of the evaluation result. - /// - /// Used only for testing. fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error> { + // `check_relation` is implemented by combining `eval_relation` and + // `check_evaluation`. let e = self.eval_relation(w, u)?; Self::check_evaluation(w, u, e) } } -/// `ArithRelationGadget` defines the in-circuit counterparts of operations -/// specified in `ArithRelation` on constraint systems. +/// [`ArithRelationGadget`] defines the in-circuit gadget for constraint system +/// operations in the same way as [`ArithRelation`]. pub trait ArithRelationGadget { + /// [`ArithRelationGadget::Evaluation`] defines the type of the evaluation + /// result returned by [`ArithRelationGadget::eval_relation`], and consumed + /// by [`ArithRelationGadget::check_evaluation`]. type Evaluation; - /// Evaluates the constraint system `self` at witness `w` and instance `u`. - /// Returns the evaluation result. + /// [`ArithRelationGadget::eval_relation`] evaluates the constraint system + /// at witness `w` and instance `u`. It returns the evaluation result. fn eval_relation(&self, w: &WVar, u: &UVar) -> Result; - /// Generates constraints for enforcing that the evaluation result is valid. - /// The witness `w` and instance `u` are also parameters, because the - /// validity check may need information contained in `w` and/or `u`. + /// [`ArithRelationGadget::check_evaluation`] checks if the evaluation + /// result is valid under the help of the witness `w` and instance `u`. fn check_evaluation(w: &WVar, u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError>; } impl> RelationGadget for A { fn check_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError> { + // `check_relation` is implemented by combining `eval_relation` and + // `check_evaluation`. let e = self.eval_relation(w, u)?; Self::check_evaluation(w, u, e) } diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index 54b1e0563..b6d908f3e 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -1,7 +1,9 @@ -use ark_ff::PrimeField; +//! This module implements in-circuit R1CS variables and relation check gadgets. + +use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; use ark_relations::gr1cs::{Namespace, SynthesisError}; -use ark_std::{One, borrow::Borrow}; +use ark_std::{One, borrow::Borrow, ops::Mul}; use super::R1CS; use crate::{ @@ -14,13 +16,18 @@ use crate::{ circuits::Assignments, }; -/// An in-circuit representation of the `R1CS` struct. +/// [`R1CSMatricesVar`] is the in-circuit variable of a given R1CS structure. +/// +/// Only the matrices are represented, while the remaining R1CS parameters are +/// constants to the circuit. +/// +/// The naming is chosen to distinguish from arkworks' `(G)R1CSVar`. #[allow(non_snake_case)] #[derive(Debug, Clone)] pub struct R1CSMatricesVar { - pub A: SparseMatrixVar, - pub B: SparseMatrixVar, - pub C: SparseMatrixVar, + A: SparseMatrixVar, + B: SparseMatrixVar, + C: SparseMatrixVar, } impl> @@ -49,20 +56,24 @@ impl R1CSMatricesVar where SparseMatrixVar: MatrixGadget, [FVar]: VectorGadget, + for<'a> &'a FVar: Mul<&'a FVar, Output = FVar>, { + /// [`R1CSMatricesVar::evaluate_at`] is the in-circuit version of + /// [`R1CS::evaluate_at`] that evaluates the R1CS variable at a given vector + /// of assignments `z`. #[allow(non_snake_case)] - pub fn eval_assignments( + pub fn evaluate_at( &self, z: Assignments>, - ) -> Result<(Vec, Vec), SynthesisError> { + ) -> Result, SynthesisError> { // Multiply Cz by z[0] (u) here, allowing this method to be reused for - // both relaxed and unrelaxed R1CS. + // both relaxed and plain R1CS. let Az = self.A.mul_vector(&z)?; let Bz = self.B.mul_vector(&z)?; let Cz = self.C.mul_vector(&z)?; let uCz = Cz.scale(&z[0])?; let AzBz = Az.hadamard(&Bz)?; - Ok((AzBz, uCz)) + AzBz.sub(&uCz) } } @@ -70,24 +81,19 @@ impl, UVar: AsRef<[FVar]>> ArithRelationGadget where SparseMatrixVar: MatrixGadget, - [FVar]: VectorGadget + EquivalenceGadget, - FVar: Clone + One, + [FVar]: VectorGadget + EquivalenceGadget<[FVar]>, + // TODO (@winderica): this will not work for our incoming decider + FVar: Clone + Zero + One, + for<'a> &'a FVar: Mul<&'a FVar, Output = FVar>, { - /// Evaluation is a tuple of two vectors (`AzBz` and `uCz`) instead of a - /// single vector `AzBz - uCz`, because subtraction is not supported for - /// `FVar = NonNativeUintVar`. - type Evaluation = (Vec, Vec); + type Evaluation = Vec; fn eval_relation(&self, w: &WVar, u: &UVar) -> Result { - self.eval_assignments((FVar::one(), u.as_ref(), w.as_ref()).into()) + self.evaluate_at((FVar::one(), u.as_ref(), w.as_ref()).into()) } - fn check_evaluation( - _w: &WVar, - _u: &UVar, - (lhs, rhs): Self::Evaluation, - ) -> Result<(), SynthesisError> { - lhs.enforce_equivalent(&rhs) + fn check_evaluation(_w: &WVar, _u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError> { + e.enforce_equivalent(&vec![FVar::zero(); e.len()]) } } diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 84c1c6278..5a65f25ec 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -1,3 +1,6 @@ +//! This module implements the Rank-1 Constraint System (R1CS) and its relation +//! checks against plain and relaxed witnesses and instances. + use ark_ff::Field; use ark_relations::gr1cs::{ConstraintSystem, Matrix, R1CS_PREDICATE_LABEL}; use ark_std::{cfg_into_iter, cfg_iter, iterable::Iterable}; @@ -12,6 +15,7 @@ use crate::{ pub mod circuits; +/// [`R1CSConfig`] stores the shape parameters of an R1CS structure. #[derive(Debug, Clone, Default, PartialEq)] pub struct R1CSConfig { m: usize, // number of constraints @@ -20,6 +24,7 @@ pub struct R1CSConfig { } impl R1CSConfig { + /// [`R1CSConfig::new`] creates a new R1CS configuration. pub fn new(n_constraints: usize, n_variables: usize, n_public_inputs: usize) -> Self { Self { m: n_constraints, @@ -54,11 +59,6 @@ impl ArithConfig for R1CSConfig { fn n_witnesses(&self) -> usize { self.n_variables() - self.n_public_inputs() - 1 } - - #[inline] - fn set_n_public_inputs(&mut self, l: usize) { - self.l = l; - } } impl From<&ConstraintSystem> for R1CSConfig { @@ -93,48 +93,62 @@ impl CCSVariant for R1CSConfig { } } +/// [`R1CS`] holds the three sparse matrices `A`, `B`, `C` together with the +/// configuration. #[allow(non_snake_case)] #[derive(Debug, Clone, Default, PartialEq)] pub struct R1CS { cfg: R1CSConfig, - pub A: Matrix, - pub B: Matrix, - pub C: Matrix, + pub(super) A: Matrix, + pub(super) B: Matrix, + pub(super) C: Matrix, } +type Row = Vec<(F, usize)>; + impl R1CS { - /// Evaluates the R1CS relation at a given vector of assignments `z` - pub fn eval_assignments( + /// [`R1CS::evaluate_rows`] evaluates the R1CS relation by applying the + /// provided function `f` to each triplet of rows `(A[i], B[i], C[i])`. + pub fn evaluate_rows( + &self, + f: impl FnMut(((&Row, &Row), &Row)) -> Result, + ) -> Result, Error> { + cfg_iter!(self.A).zip(&self.B).zip(&self.C).map(f).collect() + } + + /// [`R1CS::evaluate_at`] evaluates the R1CS relation at a given vector of + /// assignments `z`. + pub fn evaluate_at( &self, z: Assignments + Sync>, ) -> Result, Error> { + let cfg = &self.cfg; + let public_len = z.public.as_ref().len(); let private_len = z.private.as_ref().len(); - if public_len != self.n_public_inputs() { + if public_len != cfg.n_public_inputs() { return Err(Error::MalformedAssignments(format!( "The number of public inputs in R1CS ({}) does not match the length of the provided public inputs ({}).", - self.n_public_inputs(), + cfg.n_public_inputs(), public_len ))); } - if private_len != self.n_witnesses() { + if private_len != cfg.n_witnesses() { return Err(Error::MalformedAssignments(format!( "The number of witnesses in R1CS ({}) does not match the length of the provided witnesses ({}).", - self.n_witnesses(), + cfg.n_witnesses(), private_len ))); } - Ok(cfg_iter!(self.A) - .zip(&self.B) - .zip(&self.C) - .map(|((a, b), c)| { - let az = a.iter().map(|(val, col)| z[*col] * val).sum::(); - let bz = b.iter().map(|(val, col)| z[*col] * val).sum::(); - let cz = c.iter().map(|(val, col)| z[*col] * val).sum::(); - az * bz - z[0] * cz - }) - .collect()) + self.evaluate_rows(|((a, b), c)| { + let az = a.iter().map(|(val, col)| z[*col] * val).sum::(); + let bz = b.iter().map(|(val, col)| z[*col] * val).sum::(); + let cz = c.iter().map(|(val, col)| z[*col] * val).sum::(); + // use `z[0]` here since the constant term at index 0 may not be 1 + // for relaxed instances + Ok(az * bz - z[0] * cz) + }) } } @@ -153,6 +167,8 @@ impl Arith for R1CS { } impl R1CS { + /// [`R1CS::new`] creates a new R1CS structure from the given configuration + /// and matrices. #[allow(non_snake_case)] pub fn new(cfg: R1CSConfig, [A, B, C]: [Matrix; 3]) -> Self { Self { cfg, A, B, C } @@ -163,11 +179,12 @@ impl TryFrom> for R1CS { type Error = Error; fn try_from(ccs: CCS) -> Result { + let cfg = ccs.config(); Ok(Self::new( R1CSConfig::new( - ccs.n_constraints(), - ccs.n_variables(), - ccs.n_public_inputs(), + cfg.n_constraints(), + cfg.n_variables(), + cfg.n_public_inputs(), ), // `unwrap` is safe here because the type parameter T = 3 ccs.M.try_into().unwrap(), @@ -195,7 +212,7 @@ impl, U: AsRef<[F]>> ArithRelation for R1CS { type Evaluation = Vec; fn eval_relation(&self, w: &W, x: &U) -> Result { - self.eval_assignments((F::one(), x.as_ref(), w.as_ref()).into()) + self.evaluate_at((F::one(), x.as_ref(), w.as_ref()).into()) } fn check_evaluation(_w: &W, _x: &U, e: Self::Evaluation) -> Result<(), Error> { @@ -208,13 +225,23 @@ impl, U: AsRef<[F]>> ArithRelation for R1CS { } } +/// [`RelaxedWitness`] defines a relaxed version of R1CS witness. +/// +/// It is the basis of witnesses in many folding schemes that support R1CS. pub struct RelaxedWitness { + /// [`RelaxedWitness::w`] is the witness vector pub w: V, + /// [`RelaxedWitness::e`] is the error term pub e: V, } +/// [`RelaxedInstance`] defines a relaxed version of R1CS instance. +/// +/// It is the basis of instances in many folding schemes that support R1CS. pub struct RelaxedInstance { + /// [`RelaxedInstance::x`] is the public input vector pub x: V, + /// [`RelaxedInstance::u`] is the constant term pub u: V::Item, } @@ -226,7 +253,7 @@ impl ArithRelation, RelaxedInstance<&[F]>> for R1 w: &RelaxedWitness<&[F]>, u: &RelaxedInstance<&[F]>, ) -> Result { - self.eval_assignments((*u.u, u.x, w.w).into()) + self.evaluate_at((*u.u, u.x, w.w).into()) } fn check_evaluation( @@ -245,7 +272,7 @@ impl ArithRelation, RelaxedInstance<&[F]>> for R1 } #[cfg(test)] -pub mod tests { +mod tests { use ark_bn254::Fr; use ark_ff::UniformRand; use ark_relations::gr1cs::ConstraintSynthesizer; diff --git a/crates/primitives/src/circuits/mod.rs b/crates/primitives/src/circuits/mod.rs index 78e5b060d..ef7ce03cd 100644 --- a/crates/primitives/src/circuits/mod.rs +++ b/crates/primitives/src/circuits/mod.rs @@ -1,3 +1,5 @@ +//! This module defines circuits and helpers used by Sonobe. + use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, fields::fp::FpVar}; use ark_relations::gr1cs::{ @@ -8,50 +10,120 @@ use ark_std::{ ops::{Deref, Index, IndexMut}, }; -use crate::transcripts::{Absorbable, AbsorbableGadget}; +use crate::transcripts::{Absorbable, AbsorbableVar}; 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). -/// The parameter z_i denotes the current state, and z_{i+1} denotes the next state after applying -/// the step. -/// Note that the external inputs for the specific circuit are defined at the implementation of -/// both `FCircuit::ExternalInputs` and `FCircuit::ExternalInputsVar`, where the `Default` trait -/// implementation for the `ExternalInputs` returns the initialized data structure (ie. if the type -/// contains a vector, it is initialized at the expected length). +/// [`FCircuit`] defines the trait of step circuits being proven by IVC schemes. +/// +/// In IVC, a step circuit is repeatedly invoked to update some state persisted +/// throughout the execution. +/// For flexibility, we further allow each step to take some external inputs +/// and produce some external outputs that are not part of the state, which may +/// or may not be constrained inside the step circuit. +/// +/// Such a design has several advantages: +/// 1. It allows the implementation to keep the state minimal, only including +/// the parts that need to be preserved and constrained across steps, while +/// step-specific inputs that might be large are not part of the state. +/// +/// For example, in a Merkle tree update circuit, the state may only contain +/// the root of the tree, while the leaf value and authentication path which +/// are large can be provided as external inputs at each step. +/// +/// 2. The caller of the step circuit can peek into the circuit execution at +/// each step via the external outputs by having the circuit return +/// `var.value()` for desired variables. +/// +/// For example, in a Merkle tree update circuit, the circuit can return the +/// intermediate hashes computed at each step as external outputs, allowing +/// the caller to test if the hash computation is correct. +/// +/// 3. The implementation can mix out-of-circuit and in-circuit logic in this +/// structure, where the out-of-circuit logic may consume external inputs and +/// produce external outputs for the next step. +/// This is why the implementation may choose to constrain or not constrain +/// the external inputs/outputs inside the step circuit. +/// Such a mixed design can be helpful if the out-of-circuit logic and the +/// in-circuit logic are highly interdependent. +/// +/// For example, in a Merkle tree update circuit, one may write both the +/// Merkle proof generation (out-of-circuit) and verification (in-circuit) +/// logic in a single [`FCircuit::generate_step_constraints`]. +/// In this case, the external inputs contain the leaf value to be added, as +/// well as all the existing tree nodes. +/// The latter will be used by the out-of-circuit logic to compute the path, +/// but will not be constrained inside the circuit. +/// The external outputs contain the new tree nodes after the update, which +/// will be used as the inputs to the next step. +/// +/// To summarize, the step circuit takes as input the current state and some +/// external inputs, and returns the next state and some external outputs. pub trait FCircuit { + /// [`FCircuit::Field`] is the field over which the circuit is defined. type Field: PrimeField; + /// [`FCircuit::State`] is the type of the state. + /// + /// It is usually an array of field elements, but we make our design quite + /// flexible so that the implementation is free to choose any structure for + /// it. type State: Clone + PartialEq + Absorbable; + /// [`FCircuit::StateVar`] is the in-circuit variable type for the state. + /// + /// If the implementation chooses custom structures for the state, it should + /// implement the required traits for the corresponding variable type. type StateVar: GR1CSVar + AllocVar - + AbsorbableGadget; + + AbsorbableVar; + /// [`FCircuit::ExternalInputs`] is the type of external inputs provided to + /// each step of the circuit. type ExternalInputs; + /// [`FCircuit::ExternalOutputs`] is the type of external outputs produced + /// by each step of the circuit. type ExternalOutputs; + /// [`FCircuit::dummy_state`] returns a dummy state for the circuit. fn dummy_state(&self) -> Self::State; + /// [`FCircuit::dummy_external_inputs`] returns dummy external inputs for + /// the circuit. fn dummy_external_inputs(&self) -> Self::ExternalInputs; - /// generates the constraints for the step of F for the given z_i + /// [`FCircuit::generate_step_constraints`] generates the constraints for + /// the `i`-th step of invocation of the step circuit with the current state + /// `state` and external inputs `external_inputs`, producing the next state + /// and external outputs. + /// + /// ### Tips + /// + /// - Since this method uses `self`, the implementation store some (fixed) + /// info that is shared across all steps inside `self`. + /// - Variables in the implementation should be allocated as witnesses (not + /// public inputs) in the implementation. + /// - If needed, the constraint system `cs` can be accessed via `i.cs()` or + /// `state.cs()` using arkworks' [`GR1CSVar::cs`] method. fn generate_step_constraints( - // this method uses self, so that each FCircuit implementation (and different frontends) - // can hold a state if needed to store data to generate the constraints. &self, - cs: ConstraintSystemRef, i: FpVar, - z_i: Self::StateVar, - external_inputs: Self::ExternalInputs, // inputs that are not part of the state + state: Self::StateVar, + external_inputs: Self::ExternalInputs, ) -> Result<(Self::StateVar, Self::ExternalOutputs), SynthesisError>; } +/// [`Assignments`] represents a full assignment vector `z = (u, x, w)` for a +/// constraint system. #[derive(Clone, Debug, PartialEq)] pub struct Assignments { + /// [`Assignments::constant`] is the "constant" part (leading scalar) of the + /// assignment, which is usually 1 but might be relaxed in some cases. pub constant: F, + /// [`Assignments::public`] contains the public inputs. pub public: V, + /// [`Assignments::private`] contains the witnesses. pub private: V, } +/// [`AssignmentsOwned`] is a convenience alias for owned assignment vectors. pub type AssignmentsOwned = Assignments>; impl From<(F, V, V)> for Assignments { @@ -94,10 +166,14 @@ impl + AsMut<[F]>> IndexMut for Assignments { } } +/// [`ConstraintSystemExt`] wraps a `ConstraintSystemRef` with compile-time +/// flags that control whether constraint matrices (`ARITH_ENABLED`) and / or +/// assignment vectors (`ASSIGNMENTS_ENABLED`) are collected during synthesis. pub struct ConstraintSystemExt { cs: ConstraintSystemRef, } + impl Deref for ConstraintSystemExt { @@ -111,6 +187,8 @@ impl Deref impl ConstraintSystemExt { + /// [`ConstraintSystemExt::new`] creates a new constraint system wrapper + /// with the specified flags. pub fn new() -> Self { let cs = ConstraintSystem::::new_ref(); let mode = if ASSIGNMENTS_ENABLED { @@ -125,6 +203,9 @@ impl Self { cs } } + /// [`ConstraintSystemExt::execute_synthesizer`] executes a circuit inside + /// the constraint system, where the circuit should implement the + /// [`ConstraintSynthesizer`] trait. pub fn execute_synthesizer( &self, circuit: impl ConstraintSynthesizer, @@ -132,6 +213,10 @@ impl self.execute_fn(|cs| circuit.generate_constraints(cs)) } + /// [`ConstraintSystemExt::execute_fn`] executes a circuit inside the + /// constraint system, where the circuit should be defined as a closure that + /// takes as input a `ConstraintSystemRef` and returns a result of type `R`. + /// The return value of the closure will be returned by this method. pub fn execute_fn( &self, circuit: impl FnOnce(ConstraintSystemRef) -> Result, @@ -152,16 +237,25 @@ impl Defau } } +/// [`ArithExtractor`] collects only the constraint matrices (no assignments) +/// from a synthesized circuit. pub type ArithExtractor = ConstraintSystemExt; +/// [`AssignmentsExtractor`] collects only the assignments (no constraint +/// matrices) from a synthesized circuit. pub type AssignmentsExtractor = ConstraintSystemExt; impl ArithExtractor { + /// [`ArithExtractor::arith`] extracts the constraint matrices from the + /// circuit and returns them as an arithmetization / constraint system + /// structure of type `A`. pub fn arith>>(self) -> Result { Ok(self.cs.into_inner().unwrap().into()) } } impl AssignmentsExtractor { + /// [`AssignmentsExtractor::assignments`] extracts the assignments from the + /// circuit and returns them as `Assignments`. pub fn assignments(self) -> Result>, SynthesisError> { let witness = self.cs.witness_assignment()?.to_vec(); // skip the first element which is '1' diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs index cd345b9d2..0c8936161 100644 --- a/crates/primitives/src/circuits/utils.rs +++ b/crates/primitives/src/circuits/utils.rs @@ -1,7 +1,8 @@ +//! This module provides utility circuits. + use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ - alloc::AllocVar, - fields::fp::{AllocatedFp, FpVar}, + GR1CSVar, alloc::AllocVar, fields::fp::{AllocatedFp, FpVar} }; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError, Variable}; @@ -12,7 +13,13 @@ use crate::{ traits::SonobeField, }; +/// [`CircuitForTest`] implements a simple test circuit computing +/// `y = x^3 + x + 5` with 4 R1CS constraints. +/// +/// It is used in unit tests to verify constraint extraction and witness +/// generation. pub struct CircuitForTest { + /// [`CircuitForTest::x`] is the input variable `x` of the circuit. pub x: F, } @@ -64,11 +71,12 @@ impl FCircuit for CircuitForTest { fn generate_step_constraints( &self, - cs: ConstraintSystemRef, _i: FpVar, z_i: Self::StateVar, _external_inputs: Self::ExternalInputs, ) -> Result<(Self::StateVar, Self::ExternalOutputs), SynthesisError> { + let cs = z_i.cs(); + // Variable 0 (implicitly added by arkworks as 1) // Variable 1 let x = if let FpVar::Var(x) = z_i[0].clone() { @@ -105,6 +113,7 @@ impl FCircuit for CircuitForTest { } } +/// [`constraints_for_test`] returns the R1CS constraints for the test circuit. #[allow(non_snake_case)] pub fn constraints_for_test() -> R1CS { // R1CS for: x^3 + x + 5 = y (example from article @@ -131,6 +140,8 @@ pub fn constraints_for_test() -> R1CS { R1CS::::new(R1CSConfig::new(4, 6, 1), [A, B, C]) } +/// [`satisfying_assignments_for_test`] returns a satisfying assignment for the +/// test circuit given an input `x`. pub fn satisfying_assignments_for_test(x: F) -> Assignments> { Assignments::from(( F::one(), diff --git a/crates/primitives/src/commitments/mod.rs b/crates/primitives/src/commitments/mod.rs index 5e1f2f256..c2d676230 100644 --- a/crates/primitives/src/commitments/mod.rs +++ b/crates/primitives/src/commitments/mod.rs @@ -1,3 +1,5 @@ +//! Abstract traits and implementations for commitment schemes. + use ark_ff::UniformRand; use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, fields::fp::FpVar, select::CondSelectGadget}; use ark_relations::gr1cs::SynthesisError; @@ -17,37 +19,65 @@ use crate::{ ops::bits::FromBitsGadget, }, traits::{CF1, CF2, SonobeCurve, SonobeField}, - transcripts::{Absorbable, AbsorbableGadget}, + transcripts::{Absorbable, AbsorbableVar}, }; pub mod pedersen; // TODO: add back other commitment schemes +/// [`Error`] enumerates possible errors during commitment operations. #[derive(Debug, Error)] pub enum Error { - // Commitment errors + /// [`Error::MessageTooLong`] indicates that the message being committed to + /// is longer than the maximum supported length. #[error( "The message being committed to has length {1}, exceeding the maximum supported length ({0})" )] MessageTooLong(usize, usize), - #[error("Blinding factor not 0 for Commitment without hiding")] - BlindingNotZero, - #[error("Blinding factors incorrect, blinding is set to {0} but blinding values are {1}")] - IncorrectBlinding(bool, String), + /// [`Error::CommitmentVerificationFail`] indicates that the provided + /// opening does not verify against the commitment. #[error("Commitment verification failed")] CommitmentVerificationFail, } +/// [`CommitmentKey`] represents a commitment key (e.g., a vector of group +/// generators for many group-based commitment schemes). pub trait CommitmentKey: Clone { + /// [`CommitmentKey::max_scalars_len`] returns the maximum number of scalars + /// that can be committed to with this key. fn max_scalars_len(&self) -> usize; } +/// [`CommitmentDef`] provides the core type definitions of a commitment scheme, +/// defining the types of relevant cryptographic objects such as the commitment +/// key, scalars, commitments, and randomness. pub trait CommitmentDef: 'static + Clone + Debug + PartialEq + Eq { + /// [`CommitmentDef::IS_HIDING`] indicates whether the commitment scheme has + /// the hiding property. const IS_HIDING: bool; + /// [`CommitmentDef::Key`] is the type of the commitment key. type Key: CommitmentKey; + /// [`CommitmentDef::Scalar`] is the type of the scalars being committed to. + /// + /// For generality, we do not restrict this to field elements and instead + /// only bound it by necessary traits. type Scalar: Clone + Copy + Default + Debug + PartialEq + Eq + Sync + Absorbable + UniformRand; + /// [`CommitmentDef::Commitment`] is the type of the commitment. + /// + /// In the future we may introduce other commitment schemes such as those + /// based on hash functions or lattices, so we do not restrict this to be + /// group elements. type Commitment: Clone + Default + Debug + PartialEq + Eq + Sync + Absorbable; + /// [`CommitmentDef::Randomness`] is the type of the randomness used in + /// the commitment. + /// + /// Hiding commitment schemes and non-hiding schemes may have different + /// randomness types, e.g., the former holds real data, while the latter + /// is just a placeholder type. + /// + /// In this way, we can leverage the compiler to reject misuse, e.g., using + /// randomness where it is not needed, or vice versa, with a unified API. type Randomness: Clone + Copy + Default @@ -64,15 +94,28 @@ pub trait CommitmentDef: 'static + Clone + Debug + PartialEq + Eq { + Sum; } +/// [`CommitmentOps`] defines algorithms for commitment schemes. pub trait CommitmentOps: CommitmentDef { + /// [`CommitmentOps::generate_key`] defines the key generation algorithm, + /// which is a randomized algorithm that takes as input the maximum length + /// `len` of supported messages, and a randomness source `rng`, and outputs + /// the commitment key. fn generate_key(len: usize, rng: impl RngCore) -> Result; + /// [`CommitmentOps::commit`] defines the commitment generation algorithm, + /// which is a (probably) randomized algorithm that takes as input + /// commitment key `ck`, a vector of scalars `v` to be committed to, and a + /// randomness source `rng`, and outputs the commitment and the randomness. fn commit( ck: &Self::Key, v: &[Self::Scalar], rng: impl RngCore, ) -> Result<(Self::Commitment, Self::Randomness), Error>; + /// [`CommitmentOps::open`] defines the commitment opening algorithm, which + /// is a deterministic algorithm that takes as input commitment key `ck`, + /// a vector of scalars `v`, the randomness `r`, and a commitment `cm`, and + /// outputs `Ok(())` if the opening verifies, or an error otherwise. fn open( ck: &Self::Key, v: &[Self::Scalar], @@ -81,28 +124,46 @@ pub trait CommitmentOps: CommitmentDef { ) -> Result<(), Error>; } +/// [`CommitmentDefGadget`] specifies the in-circuit associated types for a +/// commitment scheme gadget. pub trait CommitmentDefGadget: Clone { + /// [`CommitmentDefGadget::ConstraintField`] is the field over which the + /// circuit running the commitment scheme is defined. type ConstraintField: SonobeField; + /// [`CommitmentDefGadget::KeyVar`] is the in-circuit variable type for the + /// commitment key. type KeyVar; - type ScalarVar: AbsorbableGadget + /// [`CommitmentDefGadget::ScalarVar`] is the in-circuit variable type for + /// the scalars being committed to. + type ScalarVar: AbsorbableVar + CondSelectGadget + FromBitsGadget - + AllocVar<::Scalar, Self::ConstraintField> - + GR1CSVar::Scalar> + + AllocVar<::Scalar, Self::ConstraintField> + + GR1CSVar::Scalar> + TwoStageFieldVar; + /// [`CommitmentDefGadget::CommitmentVar`] is the in-circuit variable type + /// for the commitment. type CommitmentVar: Clone - + AbsorbableGadget + + AbsorbableVar + CondSelectGadget - + AllocVar<::Commitment, Self::ConstraintField> - + GR1CSVar::Commitment>; - type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> - + GR1CSVar::Randomness>; - - type Native: CommitmentDef; + + AllocVar<::Commitment, Self::ConstraintField> + + GR1CSVar::Commitment>; + /// [`CommitmentDefGadget::RandomnessVar`] is the in-circuit variable type + /// for the randomness used in the commitment. + type RandomnessVar: AllocVar<::Randomness, Self::ConstraintField> + + GR1CSVar::Randomness>; + + /// [`CommitmentDefGadget::Widget`] points to the out-of-circuit commitment + /// scheme widget. + type Widget: CommitmentDef; } +/// [`CommitmentOpsGadget`] defines algorithms (majorly the opening algorithm) +/// for commitment schemes in-circuit. pub trait CommitmentOpsGadget: CommitmentDefGadget { + /// [`CommitmentOpsGadget::open`] defines the commitment opening gadget + /// that matches its out-of-circuit widget [`CommitmentOps::open`]. fn open( ck: &Self::KeyVar, v: &[Self::ScalarVar], @@ -111,22 +172,28 @@ pub trait CommitmentOpsGadget: CommitmentDefGadget { ) -> Result<(), SynthesisError>; } +/// [`GroupBasedCommitment`] is a variant of commitment schemes built on groups +/// (elliptic curves). pub trait GroupBasedCommitment: CommitmentDef::Commitment>> + CommitmentOps { + /// [`GroupBasedCommitment::Gadget1`] points to the in-circuit gadget for + /// the group-based commitment scheme over the curve's base field. type Gadget1: CommitmentOpsGadget + CommitmentDefGadget< ConstraintField = CF2, ScalarVar = EmulatedFieldVar, Self::Scalar>, CommitmentVar = ::Var, - Native = Self, + Widget = Self, >; + /// [`GroupBasedCommitment::Gadget2`] points to the in-circuit gadget for + /// the group-based commitment scheme over the curve's scalar field. type Gadget2: CommitmentDefGadget< ConstraintField = Self::Scalar, ScalarVar = FpVar, CommitmentVar = EmulatedAffineVar, - Native = Self, + Widget = Self, >; } diff --git a/crates/primitives/src/commitments/pedersen.rs b/crates/primitives/src/commitments/pedersen.rs index e51ef8edf..7d5c6c295 100644 --- a/crates/primitives/src/commitments/pedersen.rs +++ b/crates/primitives/src/commitments/pedersen.rs @@ -1,3 +1,10 @@ +//! Implementation of the Pedersen commitment scheme, including out-of-circuit +//! widgets and in-circuit gadgets. +//! +//! The Pedersen commitment to a vector `v` is computed as ` + h · r`, +//! where `g` and `h` are generators, `r` is a random scalar, and `` is +//! the multi-scalar multiplication of `g` and `v`. + use ark_r1cs_std::{ boolean::Boolean, convert::ToBitsGadget, eq::EqGadget, fields::fp::FpVar, groups::CurveVar, }; @@ -12,10 +19,12 @@ use crate::{ utils::null::Null, }; +/// [`PedersenKey`] stores the public parameters for the Pedersen commitment +/// scheme, where `H` controls whether the scheme is hiding or not. #[derive(Clone)] pub struct PedersenKey { - pub g: Vec, - pub h: C, + g: Vec, + h: C, } impl CommitmentKey for PedersenKey { @@ -58,16 +67,13 @@ impl PedersenKey { } } +/// [`Pedersen`] defines the out-of-circuit Pedersen widget, where `H` controls +/// whether the scheme is hiding or not. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Pedersen { _c: PhantomData, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PedersenEmulatedGadget { - _c: PhantomData, -} - impl CommitmentDef for Pedersen { const IS_HIDING: bool = false; @@ -86,65 +92,6 @@ impl CommitmentDef for Pedersen { type Randomness = C::ScalarField; } -impl CommitmentDefGadget for PedersenGadget { - type ConstraintField = CF2; - - type KeyVar = Vec; - - type ScalarVar = EmulatedFieldVar, CF1>; - - type CommitmentVar = C::Var; - - type RandomnessVar = Null; - - type Native = Pedersen; -} - -impl CommitmentDefGadget for PedersenGadget { - type ConstraintField = CF2; - - type KeyVar = (Vec, C::Var); - - type ScalarVar = EmulatedFieldVar, CF1>; - - type CommitmentVar = C::Var; - - type RandomnessVar = EmulatedFieldVar, CF1>; - - type Native = Pedersen; -} - -impl CommitmentDefGadget for PedersenEmulatedGadget { - type ConstraintField = CF1; - - type KeyVar = Vec, C>>; - - type ScalarVar = FpVar>; - - type CommitmentVar = EmulatedAffineVar, C>; - - type RandomnessVar = Null; - - type Native = Pedersen; -} - -impl CommitmentDefGadget for PedersenEmulatedGadget { - type ConstraintField = CF1; - - type KeyVar = ( - Vec, C>>, - EmulatedAffineVar, C>, - ); - - type ScalarVar = FpVar>; - - type CommitmentVar = EmulatedAffineVar, C>; - - type RandomnessVar = FpVar>; - - type Native = Pedersen; -} - impl GroupBasedCommitment for Pedersen { type Gadget1 = PedersenGadget; type Gadget2 = PedersenEmulatedGadget; @@ -196,98 +143,18 @@ impl CommitmentOps for Pedersen { } } +/// [`PedersenGadget`] defines the in-circuit Pedersen gadget that operates over +/// the base field of the curve and supports canonical elliptic curve point +/// variables as commitments, where `H` controls whether the scheme is hiding or +/// not. #[derive(Clone)] pub struct PedersenGadget { _c: PhantomData, } -// fn joint_scalar_mul_be>( -// p: &Projective

, -// q: &Projective

, -// bits1: impl Iterator>, -// bits2: impl Iterator>, -// ) -> Result>, SynthesisError> { -// // prepare bits decomposition -// let mut bits1 = bits1.collect::>(); -// if bits1.len() == 0 { -// return Ok(ProjectiveVar::zero()); -// } -// // Remove unnecessary constant zeros in the most-significant positions. -// bits1 = bits1 -// .into_iter() -// // We iterate from the MSB down. -// .rev() -// // Skip leading zeros, if they are constants. -// .skip_while(|b| b.is_constant() && (b.value().unwrap() == false)) -// .collect(); - -// let mut bits2 = bits2.collect::>(); -// if bits2.len() == 0 { -// return Ok(ProjectiveVar::zero()); -// } -// // Remove unnecessary constant zeros in the most-significant positions. -// bits2 = bits2 -// .into_iter() -// // We iterate from the MSB down. -// .rev() -// // Skip leading zeros, if they are constants. -// .skip_while(|b| b.is_constant() && (b.value().unwrap() == false)) -// .collect(); - -// let acc = p.double().into_affine(); -// let sum = (p + q).into_affine(); -// let diff = (p - q).into_affine(); - -// let (sum_x, sum_y) = (FpVar::Constant(sum.x), FpVar::Constant(sum.y)); -// let (diff_x, diff_y) = (FpVar::Constant(diff.x), FpVar::Constant(diff.y)); -// let (mut x, mut y) = (FpVar::Constant(acc.x), FpVar::Constant(acc.y)); - -// // double-and-add loop -// for (bit1, bit2) in (bits1.iter().rev().skip(1).rev()).zip(bits2.iter().rev().skip(1).rev()) { -// let xor = *bit1 ^ *bit2; -// let xx = xor.select(&diff_x, &sum_x)?; -// let yy = xor.select(&diff_y, &sum_y)?; -// let yy = bit1.select(&yy, &yy.negate()?)?; - -// if [&x, &y].is_constant() || ([&xx, &yy].is_constant()) { -// let p = NonZeroAffineVar::new(x.clone(), y.clone()) -// .double()? -// .add_unchecked(&NonZeroAffineVar::new(xx, yy))?; -// x = p.x; -// y = p.y; -// } else { -// let lambda_1 = (&yy - &y).mul_by_inverse_unchecked(&(&xx - &x))?; -// let lambda_1_square = lambda_1.square()?; - -// let lambda_2 = y -// .mul_by_inverse_unchecked(&(&x.double()? + &xx - &lambda_1_square))? -// .double()? -// - lambda_1; - -// let x4 = lambda_2.square()? - lambda_1_square + &xx; -// let y4 = lambda_2 * &(&x - &x4) - &y; -// x = x4; -// y = y4; -// }; -// } - -// let mut acc = NonZeroAffineVar::new(x, y); -// // last bit -// aff1_neg = aff1_neg.add_unchecked(&acc)?; -// acc = bits1[bits1.len() - 1].select(&acc, &aff1_neg)?; -// aff2_neg = aff2_neg.add_unchecked(&acc)?; -// acc = bits2[bits1.len() - 1].select(&acc, &aff2_neg)?; - -// acc.into_projective().add_mixed(&{ -// let mut p = diff; -// for _ in 0..bits1.len() - 1 { -// p = p.double()?; -// } -// NonZeroAffineVar::new(p.x, p.y.negate()?) -// }) -// } - impl PedersenGadget { + /// [`PedersenGadget::msm`] performs multi-scalar multiplication in-circuit + /// with the given generators `g` and scalar bits `v`. fn msm(g: &[C::Var], v: &[Vec>>]) -> Result { let mut res = C::Var::zero(); let n = v.len(); @@ -346,6 +213,74 @@ impl CommitmentOpsGadget for PedersenGadget { } } +/// [`PedersenEmulatedGadget`] defines the in-circuit Pedersen gadget that +/// operates over the scalar field of the curve and supports emulated elliptic +/// curve point variables as commitments, where `H` controls whether the scheme +/// is hiding or not. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PedersenEmulatedGadget { + _c: PhantomData, +} + +impl CommitmentDefGadget for PedersenGadget { + type ConstraintField = CF2; + + type KeyVar = Vec; + + type ScalarVar = EmulatedFieldVar, CF1>; + + type CommitmentVar = C::Var; + + type RandomnessVar = Null; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenGadget { + type ConstraintField = CF2; + + type KeyVar = (Vec, C::Var); + + type ScalarVar = EmulatedFieldVar, CF1>; + + type CommitmentVar = C::Var; + + type RandomnessVar = EmulatedFieldVar, CF1>; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = Vec, C>>; + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = Null; + + type Widget = Pedersen; +} + +impl CommitmentDefGadget for PedersenEmulatedGadget { + type ConstraintField = CF1; + + type KeyVar = ( + Vec, C>>, + EmulatedAffineVar, C>, + ); + + type ScalarVar = FpVar>; + + type CommitmentVar = EmulatedAffineVar, C>; + + type RandomnessVar = FpVar>; + + type Widget = Pedersen; +} + #[cfg(test)] mod tests { use ark_bn254::G1Projective; diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 8e76b75ab..ddbe91c8a 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -1,3 +1,13 @@ +#![warn(missing_docs)] + +//! This crate provides the foundational primitives used throughout Sonobe's +//! folding scheme and IVC implementations. +//! +//! It includes algebraic abstractions (fields, groups, and their in-circuit +//! emulated counterparts), constraint system arithmetizations (R1CS, CCS), +//! commitment schemes, transcript/sponge constructions, sum-check protocols, +//! and various utility types. + pub mod algebra; pub mod arithmetizations; pub mod circuits; diff --git a/crates/primitives/src/relations/mod.rs b/crates/primitives/src/relations/mod.rs index 529af6565..7c055bdbf 100644 --- a/crates/primitives/src/relations/mod.rs +++ b/crates/primitives/src/relations/mod.rs @@ -1,23 +1,42 @@ +//! This module defines the core relation traits for generic witness-instance +//! satisfaction checks and satisfying pair generation. +//! +//! These traits are intentionally generic so that different arithmetizations +//! (R1CS, CCS) and different forms (plain, relaxed) can all implement them. + use ark_relations::gr1cs::SynthesisError; use ark_std::{error::Error, rand::RngCore}; +/// [`Relation`] checks whether a witness `W` and an instance `U` satisfy the +/// specified relation. pub trait Relation { + /// [`Relation::Error`] defines the error type that may occur when checking + /// the relation. type Error: Error; - /// Checks if witness `w` and instance `u` satisfy the relation `self` + /// [`Relation::check_relation`] returns `Ok(())` when `w` and `u` satisfy + /// `self`, or an error otherwise. fn check_relation(&self, w: &W, u: &U) -> Result<(), Self::Error>; } +/// [`RelationGadget`] is the in-circuit counterpart of [`Relation`]. pub trait RelationGadget { - /// Checks if witness `w` and instance `u` satisfy the relation `self` + /// [`RelationGadget::check_relation`] generates constraints enforcing that + /// `w` and `u` satisfy the relation. fn check_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError>; } -/// `WitnessInstanceSampler` allows sampling a random witness-instance pair that -/// satisfies the relation `self`. +/// [`WitnessInstanceSampler`] allows sampling a random witness-instance pair +/// that satisfies the relation. pub trait WitnessInstanceSampler { + /// [`WitnessInstanceSampler::Source`] defines the type of the source from + /// which a satisfying pair is sampled. type Source; + + /// [`WitnessInstanceSampler::Error`] defines the error type that may occur + /// when sampling a satisfying pair. type Error: Error; + /// [`WitnessInstanceSampler::sample`] draws a random satisfying pair. fn sample(&self, source: Self::Source, rng: impl RngCore) -> Result<(W, U), Self::Error>; } diff --git a/crates/primitives/src/sumcheck/circuits.rs b/crates/primitives/src/sumcheck/circuits.rs index b79967e9e..bb91b3ee8 100644 --- a/crates/primitives/src/sumcheck/circuits.rs +++ b/crates/primitives/src/sumcheck/circuits.rs @@ -1,8 +1,37 @@ -/// Heavily inspired from testudo: https://github.com/cryptonetlab/testudo/tree/master -/// Some changes: -/// - Typings to better stick to ark_poly's API -/// - Uses `folding-schemes`' own `TranscriptVar` trait and `PoseidonTranscriptVar` struct -/// - API made closer to gadgets found in `folding-schemes` +//! In-circuit verifier gadget for the sumcheck protocol. +//! +//! The code is forked from Testudo's sumcheck circuit [implementation] and +//! modified to fit Sonobe's design & use case. +//! +//! [implementation]: https://github.com/cryptonetlab/testudo/blob/7db2d30972ce72ee7622070a1debc3b72580f4c7/src/constraints.rs#L116-L143 + +// Below we attach Testudo's original license notice. +// (Note: since the Testudo repo was forked from Microsoft's Spartan repo but no +// modifications were made to the license in Testudo, their copyright notice +// still credits Microsoft.) +// +// MIT License +// +// Copyright (c) Microsoft Corporation. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + use ark_ff::PrimeField; use ark_r1cs_std::{ eq::EqGadget, @@ -11,16 +40,28 @@ use ark_r1cs_std::{ }; use ark_relations::gr1cs::SynthesisError; -use crate::{sumcheck::utils::VPAuxInfo, transcripts::TranscriptVar}; - -pub struct IOPSumCheckGadget; - -impl IOPSumCheckGadget { +use crate::{sumcheck::utils::VPAuxInfo, transcripts::TranscriptGadget}; + +/// [`SumCheckGadget`] is the in-circuit sumcheck verifier gadget. +pub struct SumCheckGadget; + +impl SumCheckGadget { + /// [`SumCheckGadget::verify`] provides an implementation of the sumcheck + /// verification algorithm in circuit. + /// + /// Given the claimed sum `claimed_sum = z`, the proof `proofs` (i.e., round + /// polynomials `g_1, ..., g_n`), the auxiliary info `aux_info`, and the + /// transcript `transcript`. + /// It returns the final evaluation `z_{n+1} = f(r_1, ..., r_n)` and the + /// Fiat-Shamir challenges `r_1, ..., r_n`. + /// + /// It mirrors the verifier widget [`super::SumCheck::verify`] with exactly + /// the same logic. pub fn verify( - claimed_sum: FpVar, + mut claimed_sum: FpVar, proofs: &Vec>>, aux_info: &VPAuxInfo, - transcript: &mut impl TranscriptVar, + transcript: &mut impl TranscriptGadget, ) -> Result<(FpVar, Vec>), SynthesisError> { transcript.add(&FpVar::constant(F::from(aux_info.num_variables as u64)))?; transcript.add(&FpVar::constant(F::from(aux_info.max_degree as u64)))?; @@ -29,7 +70,6 @@ impl IOPSumCheckGadget { } let mut challenges = Vec::with_capacity(aux_info.num_variables); - let mut expected = claimed_sum; for coeffs in proofs { if coeffs.len() - 1 != aux_info.max_degree { @@ -39,16 +79,17 @@ impl IOPSumCheckGadget { let eval_at_zero = &coeffs[0]; let eval_at_one = coeffs.iter().sum::>(); - (eval_at_zero + eval_at_one).enforce_equal(&expected)?; + (eval_at_zero + eval_at_one).enforce_equal(&claimed_sum)?; transcript.add(&coeffs)?; let challenge = transcript.challenge_field_element()?; - expected = DensePolynomialVar::from_coefficients_slice(coeffs).evaluate(&challenge)?; + claimed_sum = + DensePolynomialVar::from_coefficients_slice(coeffs).evaluate(&challenge)?; challenges.push(challenge); } - Ok((expected, challenges)) + Ok((claimed_sum, challenges)) } } @@ -72,7 +113,7 @@ mod tests { use super::*; use crate::{ - sumcheck::{IOPSumCheck, utils::VirtualPolynomial}, + sumcheck::{SumCheck, utils::VirtualPolynomial}, transcripts::poseidon::poseidon_canonical_config, }; @@ -88,18 +129,18 @@ mod tests { let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, One::one()); let aux_info = virtual_poly.aux_info.clone(); - let (proofs, challenges, _) = IOPSumCheck::prove(virtual_poly, &mut transcript_p)?; + let (proofs, challenges, _) = SumCheck::prove(virtual_poly, &mut transcript_p)?; let poly = DensePolynomial::from_coefficients_slice(&proofs[0]); let claimed_sum = poly.evaluate(&One::one()) + poly.evaluate(&Zero::zero()); let (expected, _) = - IOPSumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v)?; + SumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v)?; let cs = ConstraintSystem::new_ref(); let mut transcript_var = PoseidonSpongeVar::new(&poseidon_config); - let (expected_var, challenges_var) = IOPSumCheckGadget::verify( + let (expected_var, challenges_var) = SumCheckGadget::verify( FpVar::new_witness(cs.clone(), || Ok(claimed_sum))?, &proofs .into_iter() diff --git a/crates/primitives/src/sumcheck/mod.rs b/crates/primitives/src/sumcheck/mod.rs index d42d78a1c..7d131c708 100644 --- a/crates/primitives/src/sumcheck/mod.rs +++ b/crates/primitives/src/sumcheck/mod.rs @@ -1,13 +1,34 @@ -// code forked from: -// https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check -// -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . +//! This module implements the sumcheck protocol and its in-circuit gadgets for +//! verification. +//! +//! The code is forked from HyperPlonk's sumcheck [implementation] and modified +//! to fit Sonobe's design & use case. +//! +//! [implementation]: https://github.com/EspressoSystems/hyperplonk/tree/main/subroutines/src/poly_iop/sum_check -//! This module implements the sum check protocol. +// Below we attach HyperPlonk's original license notice. +// +// The MIT License (MIT) +// +// Copyright (c) 2022 Espresso Systems (espressosys.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. use ark_ff::PrimeField; use ark_poly::{ @@ -27,20 +48,57 @@ use crate::transcripts::{Absorbable, Transcript}; pub mod circuits; pub mod utils; +/// [`Error`] enumerates possible errors during the sumcheck protocol. #[derive(Debug, Error)] pub enum Error { + /// [`Error::IncorrectEvaluation`] indicates that the evaluation does not + /// match the claimed value. #[error("Incorrect evaluation: claimed {0}, got {1}")] IncorrectEvaluation(String, String), + /// [`Error::UnexpectedProofLength`] indicates that the proof length does + /// not match the expected length. #[error("Incorrect proof length: expected {0}, got {1}")] UnexpectedProofLength(usize, usize), + /// [`Error::UnexpectedPolynomialDegree`] indicates that the polynomial + /// degree exceeds the expected degree. #[error("Unexpected polynomial degree: expected at most {0}, got {1}")] UnexpectedPolynomialDegree(usize, usize), } +/// [`SumCheck`] implements the sumcheck protocol. +/// +/// In the sumcheck protocol, a prover wants to convince a verifier that the sum +/// of a multilinear polynomial `f` over the Boolean hypercube equals a claimed +/// value `z`, i.e., `∑_{x_1, ..., x_n ∈ {0,1}} f(x_1, ..., x_n) = z`, without +/// having the verifier evaluate the sum themselves. +/// +/// To this end, the prover and verifier engage in `n` rounds of interaction. +/// In each round `i`, we consider a variant of the original problem: given +/// polynomial `f_i` of `n - i + 1` variables `x_i, ..., x_n` and a claim `z_i`, +/// check if `∑_{x_i, ..., x_n ∈ {0,1}} f_i(x_i, ..., x_n) = z_i`. +/// The prover and the verifier's goal is to reduce this problem to the next +/// round's problem, where the new polynomial and claim are defined as: +/// - `f_{i+1}(x_{i+1}, ..., x_n) = f_i(r_i, x_{i+1}, ..., x_n)` for a random +/// `r_i` +/// - `z_{i+1} = ∑_{x_{i+1}, ..., x_n ∈ {0,1}} f_i(r_i, x_{i+1}, ..., x_n)` +/// +/// Such a reduction is achieved by the following steps: +/// 1. The prover sends to the verifier the univariate polynomial +/// `g_i(x_i) = ∑_{x_{i+1}, ..., x_n ∈ {0,1}} f_i(x_i, x_{i+1}, ..., x_n)`. +/// 2. The verifier checks if the current claim `z_i = g_i(0) + g_i(1)`. +/// 3. The verifier sends to the prover a random challenge `r_i`. +/// 4. Both parties prepares for the next round's polynomial +/// `f_{i+1}(x_{i+1}, ..., x_n) = f_i(r_i, x_{i+1}, ..., x_n)` and claim +/// `z_{i+1} = g_i(r_i)`, until the last round where all variables are fixed. #[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] -pub struct IOPSumCheck; +pub struct SumCheck; -impl IOPSumCheck { +impl SumCheck { + /// [`SumCheck::prove`] runs the prover of the sumcheck protocol over a + /// [`VirtualPolynomial`] `poly = f` with the given `transcript`. + /// It returns the proof (i.e., round polynomials `g_1, ..., g_n`), + /// Fiat-Shamir challenges `r_1, ..., r_n`, and the final "polynomial" with + /// all variables fixed (i.e., the evaluation `f_{n+1} = f(r_1, ..., r_n)`). #[allow(clippy::type_complexity)] pub fn prove( mut poly: VirtualPolynomial, @@ -61,7 +119,7 @@ impl IOPSumCheck { let mut products_sum = vec![F::ZERO; poly.aux_info.max_degree + 1]; // Step 2: generate sum for the partial evaluated polynomial: - // f(r_1, ... r_m,, x_{m+1}... x_n) + // `f_i(x_i, ..., x_n) = f(r_1, ... r_{i-1}, x_i, ..., x_n)` poly.products.iter().for_each(|(coefficient, products)| { #[cfg(feature = "parallel")] @@ -155,6 +213,12 @@ impl IOPSumCheck { Ok((prover_msgs, challenges, poly.flattened_ml_extensions)) } + /// [`SumCheck::verify`] runs the verifier of the sumcheck protocol given + /// the claimed sum `claimed_sum = z`, the proof `proofs` (i.e., round + /// polynomials `g_1, ..., g_n`), the auxiliary info `aux_info`, and the + /// transcript `transcript`. + /// It returns the final evaluation `z_{n+1} = f(r_1, ..., r_n)` and the + /// Fiat-Shamir challenges `r_1, ..., r_n`. pub fn verify( mut claimed_sum: F, proofs: &[Vec], @@ -186,7 +250,7 @@ impl IOPSumCheck { let eval_at_one = coeffs.iter().sum::(); // the deferred check during the interactive phase: - // 1. check if the received 'P(0) + P(1) = claimed_sum`. + // 1. check if the received 'g_i(0) + g_i(1) = z_i`. if eval_at_zero + eval_at_one != claimed_sum { return Err(Error::IncorrectEvaluation( claimed_sum.to_string(), @@ -197,7 +261,7 @@ impl IOPSumCheck { transcript.add(coeffs); let challenge = transcript.challenge_field_element(); - // 2. set `expected` to `P(r)` + // 2. set next `z_{i+1}` to `g_i(r_i)` claimed_sum = DensePolynomial::from_coefficients_slice(coeffs).evaluate(&challenge); challenges.push(challenge); } @@ -207,7 +271,7 @@ impl IOPSumCheck { } #[cfg(test)] -pub mod tests { +mod tests { use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; use ark_ff::Field; use ark_pallas::Fr; @@ -220,40 +284,42 @@ pub mod tests { use crate::transcripts::poseidon::poseidon_canonical_config; #[test] - pub fn sumcheck_poseidon() -> Result<(), Error> { + fn test_sumcheck() -> Result<(), Error> { let n_vars = 10; let mut rng = thread_rng(); let poly_mle = DenseMultilinearExtension::rand(n_vars, &mut rng); - let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); - sumcheck_poseidon_opt(virtual_poly)?; + test_sumcheck_opt(poly_mle)?; // test with zero poly let poly_mle = DenseMultilinearExtension::from_evaluations_vec( n_vars, vec![Fr::zero(); 2usize.pow(n_vars as u32)], ); - let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); - sumcheck_poseidon_opt(virtual_poly)?; + test_sumcheck_opt(poly_mle)?; Ok(()) } - fn sumcheck_poseidon_opt(virtual_poly: VirtualPolynomial) -> Result<(), Error> { + fn test_sumcheck_opt(poly_mle: DenseMultilinearExtension) -> Result<(), Error> { + let virtual_poly = VirtualPolynomial::new_from_mle(poly_mle, Fr::ONE); + let aux_info = virtual_poly.aux_info.clone(); let poseidon_config = poseidon_canonical_config::(); // sum-check prove let mut transcript_p: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); - let (proofs, _, _) = IOPSumCheck::prove(virtual_poly, &mut transcript_p)?; + let (proofs, challenges_p, eval_p) = SumCheck::prove(virtual_poly, &mut transcript_p)?; // sum-check verify let poly = DensePolynomial::from_coefficients_slice(&proofs[0]); let claimed_sum = poly.evaluate(&Fr::one()) + poly.evaluate(&Fr::zero()); let mut transcript_v: PoseidonSponge = PoseidonSponge::::new(&poseidon_config); - let res_verify = IOPSumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v); + let (eval_v, challenges_v) = + SumCheck::verify(claimed_sum, &proofs, &aux_info, &mut transcript_v)?; - assert!(res_verify.is_ok()); + assert_eq!(eval_p[0].evaluate(&vec![]), eval_v); + assert_eq!(challenges_p, challenges_v); Ok(()) } } diff --git a/crates/primitives/src/sumcheck/utils.rs b/crates/primitives/src/sumcheck/utils.rs index 917ba6d2e..184e0f7e0 100644 --- a/crates/primitives/src/sumcheck/utils.rs +++ b/crates/primitives/src/sumcheck/utils.rs @@ -1,14 +1,34 @@ -// code forked from -// https://github.com/privacy-scaling-explorations/multifolding-poc/blob/main/src/espresso/virtual_polynomial.rs +//! Virtual polynomial implementation and polynomial utilities. +//! +//! The code is forked from our previous [multifolding PoC implementation], +//! which is itself forked from HyperPlonk's [virtual polynomial code]. +//! +//! [multifolding PoC implementation]: https://github.com/privacy-scaling-explorations/multifolding-poc/blob/main/src/espresso/virtual_polynomial.rs, +//! [virtual polynomial code]: https://github.com/EspressoSystems/hyperplonk/blob/main/arithmetic/src/virtual_polynomial.rs + +// Below we attach HyperPlonk's original license notice. // -// Copyright (c) 2023 Espresso Systems (espressosys.com) -// This file is part of the HyperPlonk library. - -// You should have received a copy of the MIT License -// along with the HyperPlonk library. If not, see . - -//! This module defines our main mathematical object `VirtualPolynomial`; and -//! various functions associated with it. +// The MIT License (MIT) +// +// Copyright (c) 2022 Espresso Systems (espressosys.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. use ark_ff::{Field, PrimeField, batch_inversion}; use ark_poly::{DenseMultilinearExtension, DenseUVPolynomial, univariate::DensePolynomial}; @@ -18,7 +38,7 @@ use ark_std::cfg_into_iter; #[cfg(feature = "parallel")] use rayon::prelude::*; -/// A virtual polynomial is a sum of products of multilinear polynomials; +/// [`VirtualPolynomial`] is a sum of products of multilinear polynomials; /// where the multilinear polynomials are stored via their multilinear /// extensions: `(coefficient, DenseMultilinearExtension)` /// @@ -42,19 +62,25 @@ use rayon::prelude::*; /// #[derive(Clone, Debug, Default, PartialEq)] pub struct VirtualPolynomial { - /// Aux information about the multilinear polynomial + /// [`VirtualPolynomial::aux_info`] is the aux information about the + /// multilinear polynomial. pub aux_info: VPAuxInfo, + /// [`VirtualPolynomial::flattened_ml_extensions`] stores multilinear + /// extensions in which product multiplicand can refer to. pub flattened_ml_extensions: Vec>, - /// list of reference to products (as usize) of multilinear extension + /// [`VirtualPolynomial::products`] is a list of reference to products + /// (as usize) of multilinear extension pub products: Vec<(F, Vec)>, } #[derive(Clone, Debug, Default, PartialEq, Eq, CanonicalSerialize)] -/// Auxiliary information about the multilinear polynomial +/// [`VPAuxInfo`] is auxiliary information about the multilinear polynomial. pub struct VPAuxInfo { - /// max number of multiplicands in each product + /// [`VPAuxInfo::max_degree`] is the max number of multiplicands in each + /// product. pub max_degree: usize, - /// number of variables of the polynomial + /// [`VPAuxInfo::num_variables`] is the number of variables of the + /// polynomial. pub num_variables: usize, } @@ -74,14 +100,13 @@ impl VirtualPolynomial { } } -/// `EqPoly` represents the following polynomial: -/// -/// `eq(x, y) = \prod_{i=1}^n (x_i * y_i + (1 - x_i) * (1 - y_i))` +/// [`EqPoly`] represents the multilinear equality polynomial +/// `eq(x, y) = Π_{i ∈ {0,1}} (x_i y_i + (1 - x_i)(1 - y_i))`. pub struct EqPoly; impl EqPoly { - /// This function builds `eq(x, y)` by fixing `y = r` and outputting the - /// evaluations over all `x` in `[0, 2^n)`. + /// [`EqPoly::fix_y_evals`] function evaluates `eq(x, y)` by fixing `y = r` + /// and outputting the evaluations over all `x` in `[0, 2^n)`. pub fn fix_y_evals(r: &[F]) -> Vec { // we build eq(x,r) from its evaluations // we want to evaluate eq(x,r) over all binary strings `x` of length `n` @@ -113,7 +138,7 @@ impl EqPoly { buf } - /// Evaluate eq polynomial. + /// [`EqPoly::fix_xy_eval`] evaluates `eq(x, y)` by fixing both `x` and `y`. pub fn fix_xy_eval(x: &[F], y: &[F]) -> F { debug_assert_eq!(x.len(), y.len()); x.iter() @@ -123,10 +148,12 @@ impl EqPoly { } } -pub struct EqPolyVar; +/// [`EqPolyGadget`] is the in-circuit gadget of [`EqPoly`]. +pub struct EqPolyGadget; -impl EqPolyVar { - /// Evaluate eq polynomial in circuit. +impl EqPolyGadget { + /// [`EqPolyGadget::fix_xy_eval`] evaluates `eq(x, y)` in-circuit by fixing + /// both `x` and `y`. pub fn fix_xy_eval(x: &[FpVar], y: &[FpVar]) -> FpVar { debug_assert_eq!(x.len(), y.len()); let mut eval = FpVar::::one(); @@ -137,6 +164,10 @@ impl EqPolyVar { } } +/// [`barycentric_weights`] computes the barycentric weights for a given set of +/// evaluation `points`. +/// +/// Used to extrapolate polynomial evaluations via the barycentric formula. #[allow(clippy::filter_map_bool_then)] pub fn barycentric_weights(points: &[F]) -> Vec { let mut weights = points @@ -155,6 +186,8 @@ pub fn barycentric_weights(points: &[F]) -> Vec { weights } +/// [`extrapolate`] extrapolates the polynomial defined by `(points, evals)` to +/// a new point `at`, using the precomputed barycentric `weights`. pub fn extrapolate(points: &[F], weights: &[F], evals: &[F], at: &F) -> F { let (coeffs, sum_inv) = { let mut coeffs = points.iter().map(|point| *at - point).collect::>(); @@ -173,7 +206,8 @@ pub fn extrapolate(points: &[F], weights: &[F], evals: &[F], at: * sum_inv } -/// Computes the lagrange interpolated polynomial from the given points `p_i` +/// [`compute_lagrange_interpolated_poly`] computes the lagrange interpolated +/// polynomial from the given points `p_i`. pub fn compute_lagrange_interpolated_poly(p_i: &[F]) -> DensePolynomial { let v = (0..p_i.len()) .map(|i| F::from(i as u64)) diff --git a/crates/primitives/src/traits.rs b/crates/primitives/src/traits.rs index 6162c5c4c..6ee687a85 100644 --- a/crates/primitives/src/traits.rs +++ b/crates/primitives/src/traits.rs @@ -1,9 +1,19 @@ +//! This module defines helper traits used across Sonobe's crates. + pub use crate::algebra::{ field::SonobeField, group::{CF1, CF2, SonobeCurve}, }; +/// [`Dummy`] provides a way to construct a placeholder ("dummy") value of a +/// given type, parameterized by some configuration `Cfg`. +/// +/// This is useful when initializing data structures that require a value of a +/// certain shape before the real data is available, e.g., when setting up the +/// initial state of a folding scheme. pub trait Dummy { + /// [`Dummy::dummy`] constructs a dummy value of `Self` based on the given + /// configuration `cfg`. fn dummy(cfg: Cfg) -> Self; } @@ -25,12 +35,15 @@ impl, B: Dummy> Dummy for (A, B) { } } -/// Converts a value `self` into a vector of field elements, ordered in the same -/// way as how a variable of type `Var` would be represented *natively* in the -/// circuit. +/// [`Inputize`] converts a value into a vector of field elements, ordered in +/// the same way as how the value's corresponding in-circuit variable would be +/// represented in the canonical way in the circuit when allocated as public +/// input. /// /// This is useful for the verifier to compute the public inputs. pub trait Inputize { + /// [`Inputize::inputize`] outputs the underlying field elements of `self` + /// as if it is allocated in the canonical way in-circuit. fn inputize(&self) -> Vec; } @@ -40,16 +53,19 @@ impl> Inputize for [T] { } } -/// Converts a value `self` into a vector of field elements, ordered in the same -/// way as how a variable of type `Var` would be represented *non-natively* in -/// the circuit. +/// [`InputizeEmulated`] converts a value into a vector of field elements, +/// ordered in the same way as how the value's corresponding in-circuit variable +/// would be represented in the emulated way in the circuit when allocated as +/// public input. /// /// This is useful for the verifier to compute the public inputs. /// /// Note that we require this trait because we need to distinguish between some -/// data types that are represented both natively and non-natively in-circuit -/// (e.g., field elements can have type `FpVar` and `NonNativeUintVar`). +/// data types that can be represented in both the canonical and emulated ways +/// in-circuit (e.g., field elements or elliptic curve points). pub trait InputizeEmulated { + /// [`InputizeEmulated::inputize_emulated`] outputs the underlying field + /// elements of `self` as if it is allocated in the emulated way in-circuit. fn inputize_emulated(&self) -> Vec; } diff --git a/crates/primitives/src/transcripts/absorbable.rs b/crates/primitives/src/transcripts/absorbable.rs index ba7de434c..0c2f2f7de 100644 --- a/crates/primitives/src/transcripts/absorbable.rs +++ b/crates/primitives/src/transcripts/absorbable.rs @@ -1,3 +1,9 @@ +//! This module defines traits for converting values into a form absorbable by a +//! sponge or transcript. +//! +//! Implementations are provided for some primitive types as well as composite +//! types (references, tuples, slices, etc.). + use ark_ff::PrimeField; use ark_r1cs_std::fields::fp::FpVar; use ark_relations::gr1cs::SynthesisError; @@ -35,14 +41,15 @@ use ark_relations::gr1cs::SynthesisError; // so I can define `SonobeField: for Absorbable`. // Personally I think the best option is 3. File an issue or submit a PR if you // have better solution :) +/// [`Absorbable`] is a trait for objects that can be absorbed into a sponge or +/// transcript. pub trait Absorbable { + /// [`Absorbable::absorb_into`] absorbs `self` into the given destination + /// vector of field elements. + /// + /// The implementation should append the field elements representing `self` + /// to `dest`. fn absorb_into(&self, dest: &mut Vec); - - fn to_absorbable(&self) -> Vec { - let mut result = Vec::new(); - self.absorb_into(&mut result); - result - } } impl Absorbable for usize { @@ -84,46 +91,45 @@ impl Absorbable for Vec { } } -/// An interface for objects that can be absorbed by a `TranscriptVar` whose constraint field -/// is `F`. +/// [`AbsorbableVar`] is a trait for in-circuit variables that can be absorbed +/// into a sponge or transcript defined over constraint field `F`. /// -/// Matches `AbsorbGadget` in `ark-crypto-primitives`. -pub trait AbsorbableGadget { +/// Matches [`Absorbable`]. +pub trait AbsorbableVar { + /// [`AbsorbableVar::absorb_into`] absorbs `self` into the given + /// destination vector of field element variables. + /// + /// The implementation should append the field element variables + /// representing `self` to `dest`. fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError>; - - fn to_absorbable(&self) -> Result>, SynthesisError> { - let mut result = Vec::new(); - self.absorb_into(&mut result)?; - Ok(result) - } } -impl> AbsorbableGadget for &T { +impl> AbsorbableVar for &T { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { (*self).absorb_into(dest) } } -impl> AbsorbableGadget for (T, T) { +impl> AbsorbableVar for (T, T) { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.0.absorb_into(dest)?; self.1.absorb_into(dest) } } -impl> AbsorbableGadget for [T] { +impl> AbsorbableVar for [T] { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.iter().try_for_each(|t| t.absorb_into(dest)) } } -impl, const N: usize> AbsorbableGadget for [T; N] { +impl, const N: usize> AbsorbableVar for [T; N] { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.as_ref().absorb_into(dest) } } -impl> AbsorbableGadget for Vec { +impl> AbsorbableVar for Vec { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.as_slice().absorb_into(dest) } diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs index 518d69547..5660015d2 100644 --- a/crates/primitives/src/transcripts/griffin/mod.rs +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -1,3 +1,43 @@ +//! Implementation of the Griffin circuit-friendly hash function and its +//! parameter generation, as well as out-of-circuit widgets and in-circuit +//! gadgets for permutation, hashing, sponges, and transcripts. +//! +//! According to the Griffin [paper], it is very efficient in terms of the +//! number of constraints, but later an [attack] on Griffin and similar hash +//! functions was discovered. +//! Therefore, it is recommended to avoid using Griffin in production. +//! +//! The code is forked from the [implementation] in the Hash Functions for +//! Zero-Knowledge Applications Zoo but uses arkworks instead of bellman as the +//! underlying cryptographic library. +//! +//! [paper]: https://eprint.iacr.org/2022/403.pdf +//! [attack]: https://eprint.iacr.org/2024/347.pdf +//! [implementation]: https://extgit.isec.tugraz.at/krypto/zkfriendlyhashzoo + +// Below we attach Hash functions for Zero-Knowledge applications Zoo's original +// license notice. +// +// Copyright (c) 2021 Graz University of Technology +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + use ark_ff::{LegendreSymbol, PrimeField}; use ark_r1cs_std::{ GR1CSVar, @@ -13,42 +53,28 @@ use sha3::{ pub mod sponge; -pub fn field_element_from_shake(reader: &mut impl XofReader) -> F { - let mut buf = vec![0u8; F::MODULUS_BIT_SIZE.div_ceil(8) as usize]; - - loop { - reader.read(&mut buf); - if let Some(el) = F::from_random_bytes(&buf) { - return el; - } - } -} - -pub fn field_element_from_shake_without_0(reader: &mut impl XofReader) -> F { - loop { - let element = field_element_from_shake::(reader); - if !element.is_zero() { - return element; - } - } -} - +/// [`GriffinParams`] stores the full parameterisation of the Griffin +/// permutation for a given prime field: state width `t`, S-box degree `d`, +/// number of rounds, round constants, alpha/beta constants, and the MDS-like +/// matrix. #[derive(Clone, Debug)] pub struct GriffinParams { - pub(crate) round_constants: Vec>, - pub(crate) t: usize, - pub(crate) d: usize, - pub(crate) d_inv: Vec, - pub(crate) rounds: usize, - pub(crate) alpha_beta: Vec<[F; 2]>, - pub(crate) mat: Vec>, - pub rate: usize, - pub capacity: usize, + round_constants: Vec>, + t: usize, + d: usize, + d_inv: Vec, + rounds: usize, + alpha_beta: Vec<[F; 2]>, + mat: Vec>, + rate: usize, + capacity: usize, } impl GriffinParams { - pub const INIT_SHAKE: &'static str = "Griffin"; + const INIT_SHAKE: &'static str = "Griffin"; + /// [`GriffinParams::new`] constructs new Griffin parameters with the given + /// state width `t`, S-box degree `d`, and number of rounds `rounds`. pub fn new(t: usize, d: usize, rounds: usize) -> Self { // Equivalent to `assert!(t == 3 || t % 4 == 0);`, but bypass clippy's // warning about `is_multiple_of`. @@ -58,7 +84,9 @@ impl GriffinParams { let mut shake = Self::init_shake(); - let d_inv = Self::calculate_d_inv(d as u64) + let d_inv = BigUint::from(d) + .modinv(&(-F::one()).into()) + .unwrap() .to_radix_be(2) .into_iter() .map(|i| i != 0) @@ -82,11 +110,6 @@ impl GriffinParams { } } - fn calculate_d_inv(d: u64) -> BigUint { - let p_1 = -F::one(); - BigUint::from(d).modinv(&p_1.into()).unwrap() - } - fn init_shake() -> Shake128Reader { let mut shake = Shake128::default(); shake.update(Self::INIT_SHAKE.as_bytes()); @@ -97,12 +120,36 @@ impl GriffinParams { } fn instantiate_rc(t: usize, rounds: usize, shake: &mut Shake128Reader) -> Vec> { + fn field_element_from_shake(reader: &mut impl XofReader) -> F { + let mut buf = vec![0u8; F::MODULUS_BIT_SIZE.div_ceil(8) as usize]; + + loop { + reader.read(&mut buf); + if let Some(element) = F::from_random_bytes(&buf) { + return element; + } + } + } + (0..rounds - 1) .map(|_| (0..t).map(|_| field_element_from_shake(shake)).collect()) .collect() } fn instantiate_alpha_beta(t: usize, shake: &mut Shake128Reader) -> Vec<[F; 2]> { + fn field_element_from_shake_without_0(reader: &mut impl XofReader) -> F { + let mut buf = vec![0u8; F::MODULUS_BIT_SIZE.div_ceil(8) as usize]; + + loop { + reader.read(&mut buf); + if let Some(element) = F::from_random_bytes(&buf) + && !element.is_zero() + { + return element; + } + } + } + let mut alpha_beta = Vec::with_capacity(t - 2); // random alpha/beta @@ -154,22 +201,18 @@ impl GriffinParams { alpha_beta } - fn circ_mat(row: &[F]) -> Vec> { - let t = row.len(); - let mut mat: Vec> = Vec::with_capacity(t); - let mut rot = row.to_owned(); - mat.push(rot.clone()); - for _ in 1..t { - rot.rotate_right(1); - mat.push(rot.clone()); - } - mat - } - fn instantiate_matrix(t: usize) -> Vec> { if t == 3 { let row = vec![F::from(2), F::from(1), F::from(1)]; - Self::circ_mat(&row) + let t = row.len(); + let mut mat: Vec> = Vec::with_capacity(t); + let mut rot = row.to_owned(); + mat.push(rot.clone()); + for _ in 1..t { + rot.rotate_right(1); + mat.push(rot.clone()); + } + mat } else { let row1 = vec![F::from(5), F::from(7), F::from(1), F::from(3)]; let row2 = vec![F::from(4), F::from(6), F::from(1), F::from(1)]; @@ -197,14 +240,17 @@ impl GriffinParams { } } -impl GriffinParams { - fn affine_3(&self, input: &mut [S], round: usize) { +/// [`Griffin`] implements the Griffin permutation and Griffin hash. +pub struct Griffin; + +impl Griffin { + fn affine_3(params: &GriffinParams, input: &mut [F], round: usize) { // multiplication by circ(2 1 1) is equal to state + sum(state) let mut sum = input[0]; input.iter().skip(1).for_each(|el| sum.add_assign(el)); - if round < self.rounds - 1 { - for (el, rc) in input.iter_mut().zip(self.round_constants[round].iter()) { + if round < params.rounds - 1 { + for (el, rc) in input.iter_mut().zip(params.round_constants[round].iter()) { el.add_assign(&sum); el.add_assign(rc); // add round constant } @@ -216,7 +262,7 @@ impl GriffinParams { } } - fn affine_4(&self, input: &mut [S], round: usize) { + fn affine_4(params: &GriffinParams, input: &mut [F], round: usize) { let mut t_0 = input[0]; t_0.add_assign(&input[1]); let mut t_1 = input[2]; @@ -244,25 +290,25 @@ impl GriffinParams { input[2] = t_7; input[3] = t_4; - if round < self.rounds - 1 { - for (i, rc) in input.iter_mut().zip(self.round_constants[round].iter()) { + if round < params.rounds - 1 { + for (i, rc) in input.iter_mut().zip(params.round_constants[round].iter()) { i.add_assign(rc); } } } - fn affine(&self, input: &mut [S], round: usize) { - if self.t == 3 { - self.affine_3(input, round); + fn affine(params: &GriffinParams, input: &mut [F], round: usize) { + if params.t == 3 { + Griffin::affine_3(params, input, round); return; } - if self.t == 4 { - self.affine_4(input, round); + if params.t == 4 { + Griffin::affine_4(params, input, round); return; } // first matrix - let t4 = self.t / 4; + let t4 = params.t / 4; for i in 0..t4 { let start_index = i * 4; let mut t_0 = input[start_index]; @@ -275,7 +321,7 @@ impl GriffinParams { let mut t_3 = input[start_index + 3]; t_3.double_in_place(); t_3.add_assign(&t_0); - let mut t_4: S = t_1; + let mut t_4: F = t_1; t_4.double_in_place(); t_4.double_in_place(); t_4.add_assign(&t_3); @@ -290,7 +336,7 @@ impl GriffinParams { } // second matrix - let mut stored = [S::zero(); 4]; + let mut stored = [F::zero(); 4]; for l in 0..4 { stored[l] = input[l]; for j in 1..t4 { @@ -300,17 +346,17 @@ impl GriffinParams { for i in 0..input.len() { input[i].add_assign(&stored[i % 4]); - if round < self.rounds - 1 { - input[i].add_assign(&self.round_constants[round][i]); // add round constant + if round < params.rounds - 1 { + input[i].add_assign(¶ms.round_constants[round][i]); // add round constant } } } - fn non_linear(&self, input: &mut [S]) { + fn non_linear(params: &GriffinParams, input: &mut [F]) { // first two state words input[0] = { - let mut res = S::one(); - for &i in &self.d_inv { + let mut res = F::one(); + for &i in ¶ms.d_inv { res.square_in_place(); if i { res *= input[0]; @@ -322,7 +368,7 @@ impl GriffinParams { let mut state = input[1]; input[1].square_in_place(); - match self.d { + match params.d { 3 => {} 5 => { input[1].square_in_place(); @@ -336,35 +382,47 @@ impl GriffinParams { for i in 2..input.len() { y01_i += input[0]; let l = if i == 2 { y01_i } else { y01_i + state }; - let ab = &self.alpha_beta[i - 2]; + let ab = ¶ms.alpha_beta[i - 2]; state = input[i]; input[i] *= l.square() + l * ab[0] + ab[1]; } } - pub fn permute(&self, input: &mut [S]) { - self.affine(input, self.rounds); // no RC + /// [`Griffin::permute`] applies the Griffin permutation to the given input + /// state `input` in place under parameters `params`. + pub fn permute(params: &GriffinParams, input: &mut [F]) { + Griffin::affine(params, input, params.rounds); // no RC - for r in 0..self.rounds { - self.non_linear(input); - self.affine(input, r); + for r in 0..params.rounds { + Griffin::non_linear(params, input); + Griffin::affine(params, input, r); } } - pub fn hash(&self, message: &[S]) -> S { - let mut state = vec![S::zero(); self.t]; - for chunk in message.chunks(self.rate) { + /// [`Griffin::hash`] implements the Griffin hash function based on the + /// sponge construction, which produces a single field element as the digest + /// of the given message `message` under parameters `params`. + pub fn hash(params: &GriffinParams, message: &[F]) -> F { + let mut state = vec![F::zero(); params.t]; + for chunk in message.chunks(params.rate) { for i in 0..chunk.len() { state[i] += &chunk[i]; } - self.permute(&mut state) + Griffin::permute(params, &mut state) } state[0] } } -impl GriffinParams { - fn non_linear_gadget(&self, state: &[FpVar]) -> Result>, SynthesisError> { +/// [`GriffinGadget`] implements the gadgets for Griffin permutation and Griffin +/// hash. +pub struct GriffinGadget; + +impl GriffinGadget { + fn non_linear( + params: &GriffinParams, + state: &[FpVar], + ) -> Result>, SynthesisError> { let cs = state.cs(); let mut result = state.to_owned(); // x0 @@ -373,7 +431,7 @@ impl GriffinParams { { let v = result[0].value().unwrap_or_default(); let mut res = F::one(); - for &i in &self.d_inv { + for &i in ¶ms.d_inv { res.square_in_place(); if i { res *= v; @@ -385,14 +443,14 @@ impl GriffinParams { })?; let mut sq = result[0].square()?; - if self.d == 5 { + if params.d == 5 { sq = sq.square()?; } result[0].mul_equals(&sq, &state[0])?; // x1 let mut sq = result[1].square()?; - if self.d == 5 { + if params.d == 5 { sq = sq.square()?; } result[1] *= sq; @@ -407,32 +465,37 @@ impl GriffinParams { } else { &y01_i + &state[i - 1] }; - let ab = &self.alpha_beta[i - 2]; + let ab = ¶ms.alpha_beta[i - 2]; result[i] *= l.square()? + l * ab[0] + ab[1]; } Ok(result) } - pub fn permute_gadget(&self, state: &[FpVar]) -> Result>, SynthesisError> { + /// [`GriffinGadget::permute`] applies the Griffin permutation to the given + /// input state variables `input` in place under parameters `params`. + pub fn permute( + params: &GriffinParams, + state: &[FpVar], + ) -> Result>, SynthesisError> { let mut current_state = state.to_owned(); - current_state = self + current_state = params .mat .iter() .map(|row| current_state.iter().zip(row).map(|(a, b)| a * *b).sum()) .collect(); - for r in 0..self.rounds { - current_state = self.non_linear_gadget(¤t_state)?; - current_state = self + for r in 0..params.rounds { + current_state = GriffinGadget::non_linear(params, ¤t_state)?; + current_state = params .mat .iter() .map(|row| current_state.iter().zip(row).map(|(a, b)| a * *b).sum()) .collect(); - if r < self.rounds - 1 { + if r < params.rounds - 1 { current_state = current_state .iter() - .zip(&self.round_constants[r]) + .zip(¶ms.round_constants[r]) .map(|(c, rc)| c + *rc) .collect(); } @@ -440,13 +503,19 @@ impl GriffinParams { Ok(current_state) } - pub fn hash_gadget(&self, message: &[FpVar]) -> Result, SynthesisError> { - let mut state = vec![FpVar::zero(); self.t]; - for chunk in message.chunks(self.rate) { + /// [`GriffinGadget::hash`] implements the gadget for Griffin hash based on + /// the sponge construction, which produces a single field element variable + /// as the digest of the given message `message` under parameters `params`. + pub fn hash( + params: &GriffinParams, + message: &[FpVar], + ) -> Result, SynthesisError> { + let mut state = vec![FpVar::zero(); params.t]; + for chunk in message.chunks(params.rate) { for i in 0..chunk.len() { state[i] += &chunk[i]; } - state = self.permute_gadget(&state)?; + state = GriffinGadget::permute(params, &state)?; } Ok(state[0].clone()) } @@ -466,39 +535,28 @@ mod tests { #[test] fn test() -> Result<(), Box> { let rng = &mut thread_rng(); - let griffin = GriffinParams::new(24, 5, 9); - let t = griffin.t; + let params = GriffinParams::new(24, 5, 9); + let t = params.t; let x: Vec = (0..t).map(|_| Fr::rand(rng)).collect(); - let y = griffin.hash(&x); + let y = Griffin::hash(¶ms, &x); let cs = ConstraintSystem::new_ref(); let x_var = Vec::new_witness(cs.clone(), || Ok(x.clone()))?; - let y_var = griffin.hash_gadget(&x_var)?; + let y_var = GriffinGadget::hash(¶ms, &x_var)?; assert_eq!(y, y_var.value()?); println!("{}", cs.num_constraints()); assert!(cs.is_satisfied()?); Ok(()) } -} - -#[cfg(test)] -mod griffin_tests_bn256 { - use ark_bn254::Fr; - use ark_ff::UniformRand; - use ark_std::rand::thread_rng; - - use super::*; - - static TESTRUNS: usize = 5; #[test] - fn consistent_perm() { + fn test_consistent_perm() { let rng = &mut thread_rng(); - let griffin = GriffinParams::new(3, 5, 12); - let t = griffin.t; - for _ in 0..TESTRUNS { + let params = GriffinParams::new(3, 5, 12); + let t = params.t; + for _ in 0..5 { let input1: Vec<_> = (0..t).map(|_| Fr::rand(rng)).collect(); let mut input2: Vec<_>; @@ -512,9 +570,9 @@ mod griffin_tests_bn256 { let mut perm1 = input1.clone(); let mut perm2 = input1.clone(); let mut perm3 = input2.clone(); - griffin.permute(&mut perm1); - griffin.permute(&mut perm2); - griffin.permute(&mut perm3); + Griffin::permute(¶ms, &mut perm1); + Griffin::permute(¶ms, &mut perm2); + Griffin::permute(¶ms, &mut perm3); assert_eq!(perm1, perm2); assert_ne!(perm1, perm3); } @@ -534,40 +592,40 @@ mod griffin_tests_bn256 { out } - fn affine_test(t: usize) { + fn test_affine_opt(t: usize) { let rng = &mut thread_rng(); - let griffin = GriffinParams::::new(t, 5, 1); + let params = GriffinParams::::new(t, 5, 1); - let mat = &griffin.mat; + let mat = ¶ms.mat; - for _ in 0..TESTRUNS { + for _ in 0..5 { let input: Vec = (0..t).map(|_| F::rand(rng)).collect(); // affine 1 let output1 = matmul(&input, mat); let mut output2 = input.to_owned(); - griffin.affine(&mut output2, 1); + Griffin::affine(¶ms, &mut output2, 1); assert_eq!(output1, output2); } } #[test] - fn affine_3() { - affine_test::(3); + fn test_affine_3() { + test_affine_opt::(3); } #[test] - fn affine_4() { - affine_test::(4); + fn test_affine_4() { + test_affine_opt::(4); } #[test] - fn affine_8() { - affine_test::(8); + fn test_affine_8() { + test_affine_opt::(8); } #[test] - fn affine_60() { - affine_test::(60); + fn test_affine_60() { + test_affine_opt::(60); } } diff --git a/crates/primitives/src/transcripts/griffin/sponge.rs b/crates/primitives/src/transcripts/griffin/sponge.rs index ecf2e75e5..70cfa71ee 100644 --- a/crates/primitives/src/transcripts/griffin/sponge.rs +++ b/crates/primitives/src/transcripts/griffin/sponge.rs @@ -1,3 +1,5 @@ +//! Implementation of transcript traits for Griffin sponge. + use ark_crypto_primitives::sponge::DuplexSpongeMode; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{ @@ -7,23 +9,24 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::SynthesisError; use ark_std::sync::Arc; -use crate::transcripts::{AbsorbableGadget, Transcript, TranscriptVar, griffin::GriffinParams}; +use crate::transcripts::{ + AbsorbableVar, Transcript, TranscriptGadget, + griffin::{Griffin, GriffinGadget, GriffinParams}, +}; +/// [`GriffinSponge`] is a duplex sponge built on the Griffin permutation. +/// +/// The implementation mirrors arkworks' [`ark_crypto_primitives::sponge::poseidon::PoseidonSponge`]. #[derive(Clone)] pub struct GriffinSponge { - /// Sponge Config - pub griffin: Arc>, - - // Sponge State - /// Current sponge's state (current elements in the permutation block) - pub state: Vec, - /// Current mode (whether its absorbing or squeezing) - pub mode: DuplexSpongeMode, + params: Arc>, + state: Vec, + mode: DuplexSpongeMode, } impl GriffinSponge { fn permute(&mut self) { - self.griffin.permute(&mut self.state); + Griffin::permute(&self.params, &mut self.state); } // Absorbs everything in elements, this does not end in an absorption. @@ -32,9 +35,9 @@ impl GriffinSponge { loop { // if we can finish in this call - if rate_start_index + remaining_elements.len() <= self.griffin.rate { + if rate_start_index + remaining_elements.len() <= self.params.rate { for (i, element) in remaining_elements.iter().enumerate() { - self.state[self.griffin.capacity + i + rate_start_index] += element; + self.state[self.params.capacity + i + rate_start_index] += element; } self.mode = DuplexSpongeMode::Absorbing { next_absorb_index: rate_start_index + remaining_elements.len(), @@ -43,13 +46,13 @@ impl GriffinSponge { return; } // otherwise absorb (rate - rate_start_index) elements - let num_elements_absorbed = self.griffin.rate - rate_start_index; + let num_elements_absorbed = self.params.rate - rate_start_index; for (i, element) in remaining_elements .iter() .enumerate() .take(num_elements_absorbed) { - self.state[self.griffin.capacity + i + rate_start_index] += element; + self.state[self.params.capacity + i + rate_start_index] += element; } self.permute(); // the input elements got truncated by num elements absorbed @@ -63,10 +66,10 @@ impl GriffinSponge { let mut output_remaining = output; loop { // if we can finish in this call - if rate_start_index + output_remaining.len() <= self.griffin.rate { + if rate_start_index + output_remaining.len() <= self.params.rate { output_remaining.clone_from_slice( - &self.state[self.griffin.capacity + rate_start_index - ..(self.griffin.capacity + output_remaining.len() + rate_start_index)], + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + output_remaining.len() + rate_start_index)], ); self.mode = DuplexSpongeMode::Squeezing { next_squeeze_index: rate_start_index + output_remaining.len(), @@ -74,10 +77,10 @@ impl GriffinSponge { return; } // otherwise squeeze (rate - rate_start_index) elements - let num_elements_squeezed = self.griffin.rate - rate_start_index; + let num_elements_squeezed = self.params.rate - rate_start_index; output_remaining[..num_elements_squeezed].clone_from_slice( - &self.state[self.griffin.capacity + rate_start_index - ..(self.griffin.capacity + num_elements_squeezed + rate_start_index)], + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + num_elements_squeezed + rate_start_index)], ); // Repeat with updated output slices @@ -92,21 +95,19 @@ impl GriffinSponge { } } +/// [`GriffinSpongeVar`] is the in-circuit variable of [`GriffinSponge`]. +/// +/// The implementation mirrors arkworks' [`ark_crypto_primitives::sponge::poseidon::constraints::PoseidonSpongeVar`]. #[derive(Clone)] pub struct GriffinSpongeVar { - /// Sponge Parameters - pub griffin: Arc>, - - // Sponge State - /// The sponge's state - pub state: Vec>, - /// The mode - pub mode: DuplexSpongeMode, + params: Arc>, + state: Vec>, + mode: DuplexSpongeMode, } impl GriffinSpongeVar { fn permute(&mut self) -> Result<(), SynthesisError> { - self.state = self.griffin.permute_gadget(&self.state)?; + self.state = GriffinGadget::permute(&self.params, &self.state)?; Ok(()) } @@ -118,9 +119,9 @@ impl GriffinSpongeVar { let mut remaining_elements = elements; loop { // if we can finish in this call - if rate_start_index + remaining_elements.len() <= self.griffin.rate { + if rate_start_index + remaining_elements.len() <= self.params.rate { for (i, element) in remaining_elements.iter().enumerate() { - self.state[self.griffin.capacity + i + rate_start_index] += element; + self.state[self.params.capacity + i + rate_start_index] += element; } self.mode = DuplexSpongeMode::Absorbing { next_absorb_index: rate_start_index + remaining_elements.len(), @@ -129,13 +130,13 @@ impl GriffinSpongeVar { return Ok(()); } // otherwise absorb (rate - rate_start_index) elements - let num_elements_absorbed = self.griffin.rate - rate_start_index; + let num_elements_absorbed = self.params.rate - rate_start_index; for (i, element) in remaining_elements .iter() .enumerate() .take(num_elements_absorbed) { - self.state[self.griffin.capacity + i + rate_start_index] += element; + self.state[self.params.capacity + i + rate_start_index] += element; } self.permute()?; // the input elements got truncated by num elements absorbed @@ -153,10 +154,10 @@ impl GriffinSpongeVar { let mut remaining_output = output; loop { // if we can finish in this call - if rate_start_index + remaining_output.len() <= self.griffin.rate { + if rate_start_index + remaining_output.len() <= self.params.rate { remaining_output.clone_from_slice( - &self.state[self.griffin.capacity + rate_start_index - ..(self.griffin.capacity + remaining_output.len() + rate_start_index)], + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + remaining_output.len() + rate_start_index)], ); self.mode = DuplexSpongeMode::Squeezing { next_squeeze_index: rate_start_index + remaining_output.len(), @@ -164,10 +165,10 @@ impl GriffinSpongeVar { return Ok(()); } // otherwise squeeze (rate - rate_start_index) elements - let num_elements_squeezed = self.griffin.rate - rate_start_index; + let num_elements_squeezed = self.params.rate - rate_start_index; remaining_output[..num_elements_squeezed].clone_from_slice( - &self.state[self.griffin.capacity + rate_start_index - ..(self.griffin.capacity + num_elements_squeezed + rate_start_index)], + &self.state[self.params.capacity + rate_start_index + ..(self.params.capacity + num_elements_squeezed + rate_start_index)], ); // Repeat with updated output slices and rate start index @@ -184,7 +185,7 @@ impl GriffinSpongeVar { impl Transcript for GriffinSponge { type Config = Arc>; - type Var = GriffinSpongeVar; + type Gadget = GriffinSpongeVar; fn new(parameters: &Arc>) -> Self { let state = vec![F::zero(); parameters.rate + parameters.capacity]; @@ -193,7 +194,7 @@ impl Transcript for GriffinSponge { }; Self { - griffin: parameters.clone(), + params: parameters.clone(), state, mode, } @@ -207,7 +208,7 @@ impl Transcript for GriffinSponge { match self.mode { DuplexSpongeMode::Absorbing { next_absorb_index } => { let mut absorb_index = next_absorb_index; - if absorb_index == self.griffin.rate { + if absorb_index == self.params.rate { self.permute(); absorb_index = 0; } @@ -249,7 +250,7 @@ impl Transcript for GriffinSponge { } DuplexSpongeMode::Squeezing { next_squeeze_index } => { let mut squeeze_index = next_squeeze_index; - if squeeze_index == self.griffin.rate { + if squeeze_index == self.params.rate { self.permute(); squeeze_index = 0; } @@ -261,8 +262,8 @@ impl Transcript for GriffinSponge { } } -impl TranscriptVar for GriffinSpongeVar { - type Native = GriffinSponge; +impl TranscriptGadget for GriffinSpongeVar { + type Widget = GriffinSponge; fn new(parameters: &Arc>) -> Self where @@ -275,17 +276,22 @@ impl TranscriptVar for GriffinSpongeVar { }; Self { - griffin: parameters.clone(), + params: parameters.clone(), state, mode, } } - fn add + ?Sized>( + fn add + ?Sized>( &mut self, input: &A, ) -> Result<&mut Self, SynthesisError> { - let input = input.to_absorbable()?; + let input = { + let mut result = Vec::new(); + input.absorb_into(&mut result)?; + result + }; + if input.is_empty() { return Ok(self); } @@ -293,7 +299,7 @@ impl TranscriptVar for GriffinSpongeVar { match self.mode { DuplexSpongeMode::Absorbing { next_absorb_index } => { let mut absorb_index = next_absorb_index; - if absorb_index == self.griffin.rate { + if absorb_index == self.params.rate { self.permute()?; absorb_index = 0; } @@ -336,7 +342,7 @@ impl TranscriptVar for GriffinSpongeVar { } DuplexSpongeMode::Squeezing { next_squeeze_index } => { let mut squeeze_index = next_squeeze_index; - if squeeze_index == self.griffin.rate { + if squeeze_index == self.params.rate { self.permute()?; squeeze_index = 0; } @@ -349,15 +355,12 @@ impl TranscriptVar for GriffinSpongeVar { } #[cfg(test)] -pub mod tests { - use ark_bn254::{Fq, Fr, G1Projective as G1, constraints::GVar, g1::Config}; - use ark_ec::PrimeGroup; +mod tests { + use ark_bn254::{Fq, Fr, G1Projective as G1, g1::Config}; use ark_ff::UniformRand; use ark_r1cs_std::{ - GR1CSVar, - alloc::AllocVar, - fields::fp::FpVar, - groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, + groups::curves::short_weierstrass::ProjectiveVar, }; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, rand::thread_rng}; @@ -365,110 +368,97 @@ pub mod tests { use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; - use crate::algebra::{group::emulated::EmulatedAffineVar, ops::bits::FromBits}; + use crate::algebra::group::emulated::EmulatedAffineVar; + + #[test] + fn test_challenge_field_element() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(&config); + tr.add(&Fr::from(42_u32)); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(&config); + let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } #[test] - fn test_transcript_and_transcriptvar_absorb_native_point() -> Result<(), Box> { - // use 'native' transcript + fn test_challenge_bits() -> Result<(), Box> { + let nbits = 128; + + // Create a transcript outside of the circuit let config = Arc::new(GriffinParams::::new(3, 5, 12)); let mut tr = GriffinSponge::::new(&config); - let rng = &mut thread_rng(); - - let p = G1::rand(rng); - tr.add(&p); - let c = tr.challenge_field_element(); + tr.add(&Fq::from(42_u32)); + let c = tr.challenge_bits(nbits); - // use 'gadget' transcript + // Create a transcript inside of the circuit let cs = ConstraintSystem::::new_ref(); let mut tr_var = GriffinSpongeVar::::new(&config); - let p_var = ProjectiveVar::>::new_witness(cs, || Ok(p))?; - tr_var.add(&p_var)?; - let c_var = tr_var.challenge_field_element()?; + let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_bits(nbits)?; - // assert that native & gadget transcripts return the same challenge + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge assert_eq!(c, c_var.value()?); Ok(()) } #[test] - fn test_transcript_and_transcriptvar_absorb_nonnative_point() -> Result<(), Box> { - // use 'native' transcript - let config = Arc::new(GriffinParams::::new(3, 5, 12)); - let mut tr = GriffinSponge::::new(&config); + fn test_absorb_canonical_point() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = Arc::new(GriffinParams::::new(3, 5, 12)); + let mut tr = GriffinSponge::::new(&config); let rng = &mut thread_rng(); let p = G1::rand(rng); tr.add(&p); let c = tr.challenge_field_element(); - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = GriffinSpongeVar::::new(&config); - let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = GriffinSpongeVar::::new(&config); + let p_var = ProjectiveVar::>::new_witness(cs, || Ok(p))?; tr_var.add(&p_var)?; let c_var = tr_var.challenge_field_element()?; - // assert that native & gadget transcripts return the same challenge + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge assert_eq!(c, c_var.value()?); Ok(()) } #[test] - fn test_transcript_and_transcriptvar_get_challenge() -> Result<(), Box> { - // use 'native' transcript + fn test_absorb_emulated_point() -> Result<(), Box> { + // Create a transcript outside of the circuit let config = Arc::new(GriffinParams::::new(3, 5, 12)); let mut tr = GriffinSponge::::new(&config); - tr.add(&Fr::from(42_u32)); + let rng = &mut thread_rng(); + + let p = G1::rand(rng); + tr.add(&p); let c = tr.challenge_field_element(); - // use 'gadget' transcript + // Create a transcript inside of the circuit let cs = ConstraintSystem::::new_ref(); let mut tr_var = GriffinSpongeVar::::new(&config); - let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; - tr_var.add(&v)?; + let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; let c_var = tr_var.challenge_field_element()?; - // assert that native & gadget transcripts return the same challenge + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge assert_eq!(c, c_var.value()?); Ok(()) } - - #[test] - fn test_transcript_and_transcriptvar_nbits() -> Result<(), Box> { - let nbits = 128; - - // use 'native' transcript - let config = Arc::new(GriffinParams::::new(3, 5, 12)); - let mut tr = GriffinSponge::::new(&config); - tr.add(&Fq::from(42_u32)); - - // get challenge from native transcript - let c_bits = tr.challenge_bits(nbits); - - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = GriffinSpongeVar::::new(&config); - let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; - tr_var.add(&v)?; - - // get challenge from circuit transcript - let c_var = tr_var.challenge_bits(nbits)?; - - let p = G1::generator(); - let p_var = GVar::new_witness(cs.clone(), || Ok(p))?; - - // multiply point P by the challenge in different formats, to ensure that we get the same - // result natively and in-circuit - let c = Fr::from_bits_le(&c_bits); - - // check that native c*P and in-circuit c*P using scalar_mul_le are equal - assert_eq!(p * c, p_var.scalar_mul_le(c_var.iter())?.value()?); - // check that native c*P using mul_bits_be and in-circuit c*P using scalar_mul_le are equal - // (notice the .rev to convert the LE to BE) - assert_eq!( - p.mul_bits_be(c_bits.into_iter().rev()), - p_var.scalar_mul_le(c_var.iter())?.value()? - ); - Ok(()) - } } diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index 4fdaffa78..e4ca576bc 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -1,45 +1,77 @@ +//! Abstractions of sponges and Fiat-Shamir transcripts. +//! +//! This module defines the traits that unify hash functions (Poseidon, Griffin, +//! etc.) behind a common absorb / squeeze interface suitable for building +//! non-interactive proofs. +//! +//! Concrete implementations live in the [`poseidon`] and [`griffin`] +//! sub-modules. + use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; -pub use self::absorbable::{Absorbable, AbsorbableGadget}; +pub use self::absorbable::{Absorbable, AbsorbableVar}; pub mod absorbable; pub mod griffin; pub mod poseidon; +/// [`Transcript`] is the out-of-circuit widget for transcripts and sponges. +/// +/// Provers and verifiers can use this trait to absorb messages and squeeze +/// challenges in a way that is agnostic to the underlying hash function. pub trait Transcript: Clone { + /// [`Transcript::Config`] is the configuration for the underlying hash + /// function of the transcript. type Config: Clone; - type Var: TranscriptVar; + /// [`Transcript::Gadget`] is the in-circuit gadget corresponding to this + /// widget. + type Gadget: TranscriptGadget; + + /// [`Transcript::new`] creates a new transcript / sponge under the given + /// configuration `config`. fn new(config: &Self::Config) -> Self; - /// `new_with_pp_hash` creates a new transcript / sponge with the given - /// hash of the public parameters. + /// [`Transcript::new_with_pp_hash`] is a convenience method for creating a + /// new transcript / sponge under the given configuration `config` and + /// additionally absorbing a hash of the public parameters `pp_hash`. fn new_with_pp_hash(config: &Self::Config, pp_hash: F) -> Self { let mut sponge = Self::new(config); sponge.add_field_elements(&[pp_hash]); sponge } + /// [`Transcript::add`] absorbs a message `input` that can be any type + /// implementing the [`Absorbable`] trait into the transcript / sponge. fn add(&mut self, input: &A) -> &mut Self { - let elems = input.to_absorbable(); + let mut elems = Vec::new(); + input.absorb_into(&mut elems); self.add_field_elements(&elems) } + /// [`Transcript::add_field_elements`] absorbs a message `input` that is + /// represented as field elements into the transcript / sponge. fn add_field_elements(&mut self, input: &[F]) -> &mut Self; - /// Squeeze `num_bits` bits from the sponge. + /// [`Transcript::get_bits`] squeezes `num_bits` bits from the transcript / + /// sponge. fn get_bits(&mut self, num_bits: usize) -> Vec; + /// [`Transcript::get_field_element`] squeezes a single field element from + /// the transcript / sponge. fn get_field_element(&mut self) -> F { self.get_field_elements(1)[0] } + /// [`Transcript::get_field_elements`] squeezes `num_elements` field + /// elements from the transcript / sponge. fn get_field_elements(&mut self, num_elements: usize) -> Vec; - /// Creates a new sponge with applied domain separation. + /// [`Transcript::separate_domain`] creates a new transcript / sponge by + /// applying domain separation using the provided `domain` byte sequence. fn separate_domain(&self, domain: &[u8]) -> Self { let mut new_sponge = self.clone(); @@ -56,12 +88,23 @@ pub trait Transcript: Clone { new_sponge } + /// [`Transcript::challenge_field_element`] squeezes a challenge from the + /// transcript / sponge as a field element. + /// + /// Internally, it first squeezes a field element and then absorbs it back + /// into the transcript / sponge to ensure security. fn challenge_field_element(&mut self) -> F { let c = self.get_field_elements(1); self.add_field_elements(&c); c[0] } + /// [`Transcript::challenge_bits`] squeezes a challenge from the transcript + /// / sponge as a bit vector. + /// + /// Internally, it first squeezes the bits and then absorbs packed field + /// elements formed by the bits back into the transcript / sponge to ensure + /// security. fn challenge_bits(&mut self, nbits: usize) -> Vec { let bits = self.get_bits(nbits); self.add_field_elements( @@ -74,6 +117,11 @@ pub trait Transcript: Clone { bits } + /// [`Transcript::challenge_field_elements`] squeezes `n` challenges from + /// the transcript / sponge as field elements. + /// + /// Internally, it first squeezes the field elements and then absorbs them + /// back into the transcript / sponge to ensure security. fn challenge_field_elements(&mut self, n: usize) -> Vec { let c = self.get_field_elements(n); self.add_field_elements(&c); @@ -81,15 +129,22 @@ pub trait Transcript: Clone { } } -pub trait TranscriptVar: Clone { - type Native: Transcript; +/// [`TranscriptGadget`] is the in-circuit gadget for transcripts and sponges. +pub trait TranscriptGadget: Clone { + /// [`TranscriptGadget::Widget`] points to the out-of-circuit widget for + /// this transcript gadget. + type Widget: Transcript; - fn new(config: &>::Config) -> Self; + /// [`TranscriptGadget::new`] creates a new transcript / sponge variable + /// under the given configuration `config`. + fn new(config: &>::Config) -> Self; - /// `new_with_pp_hash` creates a new transcript / sponge with the given - /// hash of the public parameters. + /// [`TranscriptGadget::new_with_pp_hash`] is a convenience method for + /// creating a new transcript / sponge variable under the given + /// configuration `config` and additionally absorbing a hash of the public + /// parameters `pp_hash`. fn new_with_pp_hash( - config: &>::Config, + config: &>::Config, pp_hash: &FpVar, ) -> Result { let mut sponge = Self::new(config); @@ -97,21 +152,31 @@ pub trait TranscriptVar: Clone { Ok(sponge) } - fn add + ?Sized>( + /// [`TranscriptGadget::add`] absorbs a message `input` that can be any type + /// implementing the [`AbsorbableGadget`] trait into the transcript / sponge + /// variable. + fn add + ?Sized>( &mut self, input: &A, ) -> Result<&mut Self, SynthesisError>; - /// Squeeze `num_bits` bits from the sponge. + /// [`TranscriptGadget::get_bits`] squeezes `num_bits` bit variables from + /// the transcript / sponge variable. fn get_bits(&mut self, num_bits: usize) -> Result>, SynthesisError>; + /// [`TranscriptGadget::get_field_element`] squeezes a single field element + /// variable from the transcript / sponge variable. fn get_field_element(&mut self) -> Result, SynthesisError> { Ok(self.get_field_elements(1)?.swap_remove(0)) } + /// [`TranscriptGadget::get_field_elements`] squeezes `num_elements` field + /// element variables from the transcript / sponge variable. fn get_field_elements(&mut self, num_elements: usize) -> Result>, SynthesisError>; - /// Creates a new sponge with applied domain separation. + /// [`TranscriptGadget::separate_domain`] creates a new transcript / sponge + /// variable by applying domain separation using the provided `domain` byte + /// sequence. fn separate_domain(&self, domain: &[u8]) -> Result { let mut new_sponge = self.clone(); @@ -128,12 +193,23 @@ pub trait TranscriptVar: Clone { Ok(new_sponge) } + /// [`TranscriptGadget::challenge_field_element`] squeezes a challenge from + /// the transcript / sponge variable as a field element variable. + /// + /// Internally, it first squeezes a field element variable and then absorbs + /// it back into the transcript / sponge variable to ensure security. fn challenge_field_element(&mut self) -> Result, SynthesisError> { let mut c = self.get_field_elements(1)?; self.add(&c[0])?; Ok(c.swap_remove(0)) } + /// [`TranscriptGadget::challenge_bits`] squeezes a challenge from the + /// transcript / sponge variable as a vector of bit variables. + /// + /// Internally, it first squeezes the bit variables and then absorbs packed + /// field element variables formed by the bit variables back into the + /// transcript / sponge variable to ensure security. fn challenge_bits(&mut self, nbits: usize) -> Result>, SynthesisError> { let bits = self.get_bits(nbits)?; self.add( @@ -145,6 +221,12 @@ pub trait TranscriptVar: Clone { Ok(bits) } + /// [`TranscriptGadget::challenge_field_elements`] squeezes `n` challenges + /// from the transcript / sponge variable as field element variables. + /// + /// Internally, it first squeezes the field element variables and then + /// absorbs them back into the transcript / sponge variable to ensure + /// security. fn challenge_field_elements(&mut self, n: usize) -> Result>, SynthesisError> { let c = self.get_field_elements(n)?; self.add(&c)?; diff --git a/crates/primitives/src/transcripts/poseidon/mod.rs b/crates/primitives/src/transcripts/poseidon/mod.rs index 5693fb77c..54bdbaff8 100644 --- a/crates/primitives/src/transcripts/poseidon/mod.rs +++ b/crates/primitives/src/transcripts/poseidon/mod.rs @@ -1,9 +1,12 @@ +//! Poseidon-based transcript configurations and implementations. + use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, find_poseidon_ark_and_mds}; use ark_ff::PrimeField; pub mod sponge; -/// This Poseidon configuration generator produces a Poseidon configuration with custom parameters +/// [`poseidon_custom_config`] produces a Poseidon configuration with custom +/// parameters. pub fn poseidon_custom_config( full_rounds: usize, partial_rounds: usize, @@ -22,7 +25,9 @@ pub fn poseidon_custom_config( PoseidonConfig::new(full_rounds, partial_rounds, alpha, mds, ark, rate, capacity) } -/// This Poseidon configuration generator agrees with Circom's Poseidon(4) in the case of BN254's scalar field +/// [`poseidon_canonical_config`] produces a Poseidon configuration with default +/// parameters, which agrees with Circom's Poseidon(4) when `F` is the scalar +/// field of BN254. pub fn poseidon_canonical_config() -> PoseidonConfig { // 120 bit security target as in // https://eprint.iacr.org/2019/458.pdf diff --git a/crates/primitives/src/transcripts/poseidon/sponge.rs b/crates/primitives/src/transcripts/poseidon/sponge.rs index e595247c8..89068655b 100644 --- a/crates/primitives/src/transcripts/poseidon/sponge.rs +++ b/crates/primitives/src/transcripts/poseidon/sponge.rs @@ -1,3 +1,5 @@ +//! Implementation of transcript traits for arkworks' Poseidon sponge. + use ark_crypto_primitives::sponge::{ Absorb, CryptographicSponge, FieldBasedCryptographicSponge, constraints::CryptographicSpongeVar, @@ -8,11 +10,11 @@ use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; use ark_std::mem::transmute_copy; -use crate::transcripts::{AbsorbableGadget, Transcript, TranscriptVar}; +use crate::transcripts::{AbsorbableVar, Transcript, TranscriptGadget}; impl Transcript for PoseidonSponge { type Config = PoseidonConfig; - type Var = PoseidonSpongeVar; + type Gadget = PoseidonSpongeVar; fn new(config: &Self::Config) -> Self { CryptographicSponge::new(config) @@ -47,8 +49,8 @@ impl Transcript for PoseidonSponge { } } -impl TranscriptVar for PoseidonSpongeVar { - type Native = PoseidonSponge; +impl TranscriptGadget for PoseidonSpongeVar { + type Widget = PoseidonSponge; fn new(config: &PoseidonConfig) -> Self where @@ -57,11 +59,14 @@ impl TranscriptVar for PoseidonSpongeVar { CryptographicSpongeVar::new(ConstraintSystemRef::None, config) } - fn add + ?Sized>( + fn add + ?Sized>( &mut self, input: &A, ) -> Result<&mut Self, SynthesisError> { - self.absorb(&input.to_absorbable()?)?; + let mut result = Vec::new(); + input.absorb_into(&mut result)?; + + self.absorb(&result)?; Ok(self) } @@ -75,16 +80,13 @@ impl TranscriptVar for PoseidonSpongeVar { } #[cfg(test)] -pub mod tests { - use ark_bn254::{Fq, Fr, G1Projective as G1, constraints::GVar, g1::Config}; +mod tests { + use ark_bn254::{Fq, Fr, G1Projective as G1, g1::Config}; use ark_crypto_primitives::sponge::poseidon::{PoseidonSponge, constraints::PoseidonSpongeVar}; - use ark_ec::PrimeGroup; use ark_ff::UniformRand; use ark_r1cs_std::{ - GR1CSVar, - alloc::AllocVar, - fields::fp::FpVar, - groups::{CurveVar, curves::short_weierstrass::ProjectiveVar}, + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, + groups::curves::short_weierstrass::ProjectiveVar, }; use ark_relations::gr1cs::ConstraintSystem; use ark_std::{error::Error, rand::thread_rng, str::FromStr}; @@ -92,8 +94,8 @@ pub mod tests { use wasm_bindgen_test::wasm_bindgen_test as test; use crate::{ - algebra::{group::emulated::EmulatedAffineVar, ops::bits::FromBits}, - transcripts::{Transcript, TranscriptVar, poseidon::poseidon_canonical_config}, + algebra::group::emulated::EmulatedAffineVar, + transcripts::{Transcript, TranscriptGadget, poseidon::poseidon_canonical_config}, }; // Test with value taken from https://github.com/iden3/circomlibjs/blob/43cc582b100fc3459cf78d903a6f538e5d7f38ee/test/poseidon.js#L32 @@ -118,107 +120,94 @@ pub mod tests { } #[test] - fn test_transcript_and_transcriptvar_absorb_native_point() -> Result<(), Box> { - // use 'native' transcript + fn test_challenge_field_element() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = poseidon_canonical_config::(); + let mut tr = PoseidonSponge::::new(&config); + tr.add(&Fr::from(42_u32)); + let c = tr.challenge_field_element(); + + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = PoseidonSpongeVar::::new(&config); + let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_field_element()?; + + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge + assert_eq!(c, c_var.value()?); + Ok(()) + } + + #[test] + fn test_challenge_bits() -> Result<(), Box> { + let nbits = 128; + + // Create a transcript outside of the circuit let config = poseidon_canonical_config::(); let mut tr = PoseidonSponge::::new(&config); - let rng = &mut thread_rng(); - - let p = G1::rand(rng); - tr.add(&p); - let c = tr.challenge_field_element(); + tr.add(&Fq::from(42_u32)); + let c = tr.challenge_bits(nbits); - // use 'gadget' transcript + // Create a transcript inside of the circuit let cs = ConstraintSystem::::new_ref(); let mut tr_var = PoseidonSpongeVar::::new(&config); - let p_var = ProjectiveVar::>::new_witness(cs, || Ok(p))?; - tr_var.add(&p_var)?; - let c_var = tr_var.challenge_field_element()?; + let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; + tr_var.add(&v)?; + let c_var = tr_var.challenge_bits(nbits)?; - // assert that native & gadget transcripts return the same challenge + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge assert_eq!(c, c_var.value()?); Ok(()) } #[test] - fn test_transcript_and_transcriptvar_absorb_nonnative_point() -> Result<(), Box> { - // use 'native' transcript - let config = poseidon_canonical_config::(); - let mut tr = PoseidonSponge::::new(&config); + fn test_absorb_canonical_point() -> Result<(), Box> { + // Create a transcript outside of the circuit + let config = poseidon_canonical_config::(); + let mut tr = PoseidonSponge::::new(&config); let rng = &mut thread_rng(); let p = G1::rand(rng); tr.add(&p); let c = tr.challenge_field_element(); - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(&config); - let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; + // Create a transcript inside of the circuit + let cs = ConstraintSystem::::new_ref(); + let mut tr_var = PoseidonSpongeVar::::new(&config); + let p_var = ProjectiveVar::>::new_witness(cs, || Ok(p))?; tr_var.add(&p_var)?; let c_var = tr_var.challenge_field_element()?; - // assert that native & gadget transcripts return the same challenge + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge assert_eq!(c, c_var.value()?); Ok(()) } #[test] - fn test_transcript_and_transcriptvar_get_challenge() -> Result<(), Box> { - // use 'native' transcript + fn test_absorb_emulated_point() -> Result<(), Box> { + // Create a transcript outside of the circuit let config = poseidon_canonical_config::(); let mut tr = PoseidonSponge::::new(&config); - tr.add(&Fr::from(42_u32)); + let rng = &mut thread_rng(); + + let p = G1::rand(rng); + tr.add(&p); let c = tr.challenge_field_element(); - // use 'gadget' transcript + // Create a transcript inside of the circuit let cs = ConstraintSystem::::new_ref(); let mut tr_var = PoseidonSpongeVar::::new(&config); - let v = FpVar::::new_witness(cs.clone(), || Ok(Fr::from(42_u32)))?; - tr_var.add(&v)?; + let p_var = EmulatedAffineVar::new_witness(cs, || Ok(p))?; + tr_var.add(&p_var)?; let c_var = tr_var.challenge_field_element()?; - // assert that native & gadget transcripts return the same challenge + // Assert that in-circuit and out-of-circuit transcripts return the same + // challenge assert_eq!(c, c_var.value()?); Ok(()) } - - #[test] - fn test_transcript_and_transcriptvar_nbits() -> Result<(), Box> { - let nbits = 128; - - // use 'native' transcript - let config = poseidon_canonical_config::(); - let mut tr = PoseidonSponge::::new(&config); - tr.add(&Fq::from(42_u32)); - - // get challenge from native transcript - let c_bits = tr.challenge_bits(nbits); - - // use 'gadget' transcript - let cs = ConstraintSystem::::new_ref(); - let mut tr_var = PoseidonSpongeVar::::new(&config); - let v = FpVar::::new_witness(cs.clone(), || Ok(Fq::from(42_u32)))?; - tr_var.add(&v)?; - - // get challenge from circuit transcript - let c_var = tr_var.challenge_bits(nbits)?; - - let p = G1::generator(); - let p_var = GVar::new_witness(cs.clone(), || Ok(p))?; - - // multiply point P by the challenge in different formats, to ensure that we get the same - // result natively and in-circuit - let c = Fr::from_bits_le(&c_bits); - - // check that native c*P and in-circuit c*P using scalar_mul_le are equal - assert_eq!(p * c, p_var.scalar_mul_le(c_var.iter())?.value()?); - // check that native c*P using mul_bits_be and in-circuit c*P using scalar_mul_le are equal - // (notice the .rev to convert the LE to BE) - assert_eq!( - p.mul_bits_be(c_bits.into_iter().rev()), - p_var.scalar_mul_le(c_var.iter())?.value()? - ); - Ok(()) - } } diff --git a/crates/primitives/src/utils/mod.rs b/crates/primitives/src/utils/mod.rs index 2cf111496..7f61a9721 100644 --- a/crates/primitives/src/utils/mod.rs +++ b/crates/primitives/src/utils/mod.rs @@ -1 +1,3 @@ +//! Miscellaneous utilities shared across the primitives crate. + pub mod null; diff --git a/crates/primitives/src/utils/null.rs b/crates/primitives/src/utils/null.rs index bc6a9bb09..bce182be9 100644 --- a/crates/primitives/src/utils/null.rs +++ b/crates/primitives/src/utils/null.rs @@ -1,3 +1,6 @@ +//! This module defines a zero-cost placeholder type that have well-defined +//! arithmetic operations. + use ark_ff::Field; use ark_r1cs_std::{ GR1CSVar, @@ -11,6 +14,11 @@ use ark_std::{ ops::{Add, Mul}, }; +/// [`Null`] is a zero-sized type that absorbs any arithmetic and always returns +/// itself. +/// +/// It also has itself as its in-circuit representation, which does not allocate +/// any variables or require any constraints. #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] pub struct Null; From 79e578bfcad3fea6cfe80043e1a697a2f4c8daf0 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:10:50 +0800 Subject: [PATCH 25/93] Bring back R1CS and CCS tests --- .../src/arithmetizations/ccs/mod.rs | 68 +++++++++++++++++- .../src/arithmetizations/r1cs/circuits.rs | 69 ++++++++++++++++++- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/crates/primitives/src/arithmetizations/ccs/mod.rs b/crates/primitives/src/arithmetizations/ccs/mod.rs index 9ef789298..cebed30fe 100644 --- a/crates/primitives/src/arithmetizations/ccs/mod.rs +++ b/crates/primitives/src/arithmetizations/ccs/mod.rs @@ -262,4 +262,70 @@ impl From> for CCS { } } -// TODO: add back tests +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_ff::{One, UniformRand, Zero}; + use ark_std::{error::Error, rand::thread_rng}; + + use super::*; + use crate::{ + circuits::utils::{constraints_for_test, satisfying_assignments_for_test}, + relations::Relation, + }; + + #[test] + fn test_eval() -> Result<(), Box> { + let mut rng = thread_rng(); + let ccs: CCS = constraints_for_test::().into(); + + assert!( + ccs.evaluate_at(satisfying_assignments_for_test(Fr::rand(&mut rng)))? + .into_iter() + .all(|e| e.is_zero()) + ); + assert!( + !ccs.evaluate_at(Assignments::from(( + Fr::one(), + vec![Fr::rand(&mut rng)], + vec![ + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + ], + )))? + .into_iter() + .all(|e| e.is_zero()) + ); + + Ok(()) + } + + #[test] + fn test_check() -> Result<(), Box> { + let mut rng = thread_rng(); + let ccs: CCS = constraints_for_test::().into(); + + let assignments = satisfying_assignments_for_test(Fr::rand(&mut rng)); + + assert!( + ccs.check_relation(&assignments.private, &assignments.public) + .is_ok() + ); + assert!( + ccs.check_relation( + &[ + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + ], + &[Fr::rand(&mut rng)] + ) + .is_err() + ); + + Ok(()) + } +} diff --git a/crates/primitives/src/arithmetizations/r1cs/circuits.rs b/crates/primitives/src/arithmetizations/r1cs/circuits.rs index b6d908f3e..0d983a4a8 100644 --- a/crates/primitives/src/arithmetizations/r1cs/circuits.rs +++ b/crates/primitives/src/arithmetizations/r1cs/circuits.rs @@ -97,4 +97,71 @@ where } } -// TODO: add back tests +#[cfg(test)] +mod tests { + use ark_bn254::Fr; + use ark_ff::{One, UniformRand, Zero}; + use ark_std::{error::Error, rand::thread_rng}; + + use super::*; + use crate::{ + circuits::utils::{constraints_for_test, satisfying_assignments_for_test}, + relations::Relation, + }; + + #[test] + fn test_eval() -> Result<(), Box> { + let mut rng = thread_rng(); + let r1cs = constraints_for_test::(); + + assert!( + r1cs.evaluate_at(satisfying_assignments_for_test(Fr::rand(&mut rng)))? + .into_iter() + .all(|e| e.is_zero()) + ); + assert!( + !r1cs + .evaluate_at(Assignments::from(( + Fr::one(), + vec![Fr::rand(&mut rng)], + vec![ + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + ], + )))? + .into_iter() + .all(|e| e.is_zero()) + ); + + Ok(()) + } + + #[test] + fn test_check() -> Result<(), Box> { + let mut rng = thread_rng(); + let r1cs = constraints_for_test::(); + + let assignments = satisfying_assignments_for_test(Fr::rand(&mut rng)); + + assert!( + r1cs.check_relation(&assignments.private, &assignments.public) + .is_ok() + ); + assert!( + r1cs.check_relation( + &[ + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + Fr::rand(&mut rng), + ], + &[Fr::rand(&mut rng)] + ) + .is_err() + ); + + Ok(()) + } +} From 6f918ba53d48208afc5d834be4587a893f71be0b Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:48:16 +0800 Subject: [PATCH 26/93] fmt --- crates/primitives/src/algebra/ops/bits.rs | 4 ++-- crates/primitives/src/algebra/ops/poly.rs | 5 +++-- crates/primitives/src/arithmetizations/r1cs/mod.rs | 5 +---- crates/primitives/src/circuits/utils.rs | 4 +++- crates/primitives/src/sumcheck/circuits.rs | 2 +- crates/primitives/src/sumcheck/utils.rs | 2 +- crates/primitives/src/transcripts/griffin/mod.rs | 2 +- crates/primitives/src/transcripts/mod.rs | 10 ++++------ crates/primitives/src/utils/null.rs | 2 +- 9 files changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/primitives/src/algebra/ops/bits.rs b/crates/primitives/src/algebra/ops/bits.rs index eb997e259..7616d08fc 100644 --- a/crates/primitives/src/algebra/ops/bits.rs +++ b/crates/primitives/src/algebra/ops/bits.rs @@ -36,13 +36,13 @@ pub trait FromBitsGadget: Sized { pub trait ToBitsGadgetExt: Sized { /// [`ToBitsGadgetExt::to_n_bits_le`] decomposes `self` into `n` /// little-endian bits. - /// + /// /// An error is returned if `self` cannot be represented in `n` bits. fn to_n_bits_le(&self, n: usize) -> Result>, SynthesisError>; /// [`ToBitsGadgetExt::enforce_bit_length`] enforces that `self` can be /// represented in at most `n` bits. - /// + /// /// This is useful for checking that a field element is within the range of /// `[0, 2^n - 1]` fn enforce_bit_length(&self, n: usize) -> Result<(), SynthesisError> { diff --git a/crates/primitives/src/algebra/ops/poly.rs b/crates/primitives/src/algebra/ops/poly.rs index fed1992fd..10a5c9cea 100644 --- a/crates/primitives/src/algebra/ops/poly.rs +++ b/crates/primitives/src/algebra/ops/poly.rs @@ -38,9 +38,10 @@ pub trait EvaluationDomainGadget { /// [`EvaluationDomainGadget::evaluate_vanishing_polynomial_var`] evaluates /// the vanishing polynomial of the domain at `tau`. - /// + /// /// It is the in-circuit counterpart of [`EvaluationDomain::evaluate_vanishing_polynomial`]. - fn evaluate_vanishing_polynomial_var(&self, tau: &FpVar) -> Result, SynthesisError>; + fn evaluate_vanishing_polynomial_var(&self, tau: &FpVar) + -> Result, SynthesisError>; } impl EvaluationDomainGadget for GeneralEvaluationDomain { diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index 5a65f25ec..c38b8c547 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -118,10 +118,7 @@ impl R1CS { /// [`R1CS::evaluate_at`] evaluates the R1CS relation at a given vector of /// assignments `z`. - pub fn evaluate_at( - &self, - z: Assignments + Sync>, - ) -> Result, Error> { + pub fn evaluate_at(&self, z: Assignments + Sync>) -> Result, Error> { let cfg = &self.cfg; let public_len = z.public.as_ref().len(); diff --git a/crates/primitives/src/circuits/utils.rs b/crates/primitives/src/circuits/utils.rs index 0c8936161..464d248d3 100644 --- a/crates/primitives/src/circuits/utils.rs +++ b/crates/primitives/src/circuits/utils.rs @@ -2,7 +2,9 @@ use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ - GR1CSVar, alloc::AllocVar, fields::fp::{AllocatedFp, FpVar} + GR1CSVar, + alloc::AllocVar, + fields::fp::{AllocatedFp, FpVar}, }; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError, Variable}; diff --git a/crates/primitives/src/sumcheck/circuits.rs b/crates/primitives/src/sumcheck/circuits.rs index bb91b3ee8..60075d9a3 100644 --- a/crates/primitives/src/sumcheck/circuits.rs +++ b/crates/primitives/src/sumcheck/circuits.rs @@ -48,7 +48,7 @@ pub struct SumCheckGadget; impl SumCheckGadget { /// [`SumCheckGadget::verify`] provides an implementation of the sumcheck /// verification algorithm in circuit. - /// + /// /// Given the claimed sum `claimed_sum = z`, the proof `proofs` (i.e., round /// polynomials `g_1, ..., g_n`), the auxiliary info `aux_info`, and the /// transcript `transcript`. diff --git a/crates/primitives/src/sumcheck/utils.rs b/crates/primitives/src/sumcheck/utils.rs index 184e0f7e0..cccac6eb7 100644 --- a/crates/primitives/src/sumcheck/utils.rs +++ b/crates/primitives/src/sumcheck/utils.rs @@ -166,7 +166,7 @@ impl EqPolyGadget { /// [`barycentric_weights`] computes the barycentric weights for a given set of /// evaluation `points`. -/// +/// /// Used to extrapolate polynomial evaluations via the barycentric formula. #[allow(clippy::filter_map_bool_then)] pub fn barycentric_weights(points: &[F]) -> Vec { diff --git a/crates/primitives/src/transcripts/griffin/mod.rs b/crates/primitives/src/transcripts/griffin/mod.rs index 5660015d2..dbf1c7b64 100644 --- a/crates/primitives/src/transcripts/griffin/mod.rs +++ b/crates/primitives/src/transcripts/griffin/mod.rs @@ -6,7 +6,7 @@ //! number of constraints, but later an [attack] on Griffin and similar hash //! functions was discovered. //! Therefore, it is recommended to avoid using Griffin in production. -//! +//! //! The code is forked from the [implementation] in the Hash Functions for //! Zero-Knowledge Applications Zoo but uses arkworks instead of bellman as the //! underlying cryptographic library. diff --git a/crates/primitives/src/transcripts/mod.rs b/crates/primitives/src/transcripts/mod.rs index e4ca576bc..1ddf1c29a 100644 --- a/crates/primitives/src/transcripts/mod.rs +++ b/crates/primitives/src/transcripts/mod.rs @@ -155,10 +155,8 @@ pub trait TranscriptGadget: Clone { /// [`TranscriptGadget::add`] absorbs a message `input` that can be any type /// implementing the [`AbsorbableGadget`] trait into the transcript / sponge /// variable. - fn add + ?Sized>( - &mut self, - input: &A, - ) -> Result<&mut Self, SynthesisError>; + fn add + ?Sized>(&mut self, input: &A) + -> Result<&mut Self, SynthesisError>; /// [`TranscriptGadget::get_bits`] squeezes `num_bits` bit variables from /// the transcript / sponge variable. @@ -206,7 +204,7 @@ pub trait TranscriptGadget: Clone { /// [`TranscriptGadget::challenge_bits`] squeezes a challenge from the /// transcript / sponge variable as a vector of bit variables. - /// + /// /// Internally, it first squeezes the bit variables and then absorbs packed /// field element variables formed by the bit variables back into the /// transcript / sponge variable to ensure security. @@ -223,7 +221,7 @@ pub trait TranscriptGadget: Clone { /// [`TranscriptGadget::challenge_field_elements`] squeezes `n` challenges /// from the transcript / sponge variable as field element variables. - /// + /// /// Internally, it first squeezes the field element variables and then /// absorbs them back into the transcript / sponge variable to ensure /// security. diff --git a/crates/primitives/src/utils/null.rs b/crates/primitives/src/utils/null.rs index bce182be9..da179385b 100644 --- a/crates/primitives/src/utils/null.rs +++ b/crates/primitives/src/utils/null.rs @@ -16,7 +16,7 @@ use ark_std::{ /// [`Null`] is a zero-sized type that absorbs any arithmetic and always returns /// itself. -/// +/// /// It also has itself as its in-circuit representation, which does not allocate /// any variables or require any constraints. #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] From 1229c5cedab510cadb64347bcd459e550879b965 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:50:27 +0800 Subject: [PATCH 27/93] Fix missing trait bounds when enabling parallel feature --- crates/primitives/src/arithmetizations/r1cs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/primitives/src/arithmetizations/r1cs/mod.rs b/crates/primitives/src/arithmetizations/r1cs/mod.rs index c38b8c547..54be091c0 100644 --- a/crates/primitives/src/arithmetizations/r1cs/mod.rs +++ b/crates/primitives/src/arithmetizations/r1cs/mod.rs @@ -111,7 +111,7 @@ impl R1CS { /// provided function `f` to each triplet of rows `(A[i], B[i], C[i])`. pub fn evaluate_rows( &self, - f: impl FnMut(((&Row, &Row), &Row)) -> Result, + f: impl Fn(((&Row, &Row), &Row)) -> Result + Send + Sync, ) -> Result, Error> { cfg_iter!(self.A).zip(&self.B).zip(&self.C).map(f).collect() } From df327df60e8550f67506841c247bc69c910fe984 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 17:21:44 +0800 Subject: [PATCH 28/93] Reorganize cargo.toml --- Cargo.toml | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d1e89a20..916915046 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,28 +4,6 @@ members = [ ] resolver = "2" -[patch.crates-io] -# We depend on git versions of arkworks crates, but some of our dependencies -# depend on crates.io versions, so we need to override them here to avoid -# version conflicts. -ark-ff = { git = "https://github.com/arkworks-rs/algebra" } -ark-ec = { git = "https://github.com/arkworks-rs/algebra" } -ark-serialize = { git = "https://github.com/arkworks-rs/algebra" } -ark-poly = { git = "https://github.com/arkworks-rs/algebra" } -ark-std = { git = "https://github.com/arkworks-rs/std" } -ark-crypto-primitives = { git = "https://github.com/winderica/crypto-primitives", rev = "af003fc" } -ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" } # "sw-fix-updated" branch - -# Curve crates also need git versions -ark-bn254 = { git = "https://github.com/arkworks-rs/algebra" } -ark-grumpkin = { git = "https://github.com/arkworks-rs/algebra" } - -[patch."https://github.com/arkworks-rs/crypto-primitives"] -ark-crypto-primitives = { git = "https://github.com/winderica/crypto-primitives", rev = "af003fc" } - -[patch."https://github.com/arkworks-rs/r1cs-std"] -ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" } # "sw-fix-updated" branch - [workspace.package] edition = "2024" license = "MIT" @@ -37,7 +15,6 @@ num-bigint = { version = "0.4.3" } num-integer = { version = "0.1" } num-traits = { version = "0.2" } sha3 = { version = "0.10" } -rand = { version = "0.8.5" } rayon = { version = "1" } thiserror = { version = "2.0.16" } wasm-bindgen-test = { version = "0.3" } @@ -63,3 +40,24 @@ ark-vesta = { git = "https://github.com/arkworks-rs/algebra", default-features = # Local crates sonobe-primitives = { path = "crates/primitives", default-features = false } + +[patch.crates-io] +# We depend on git versions of arkworks crates, but some of our dependencies +# depend on crates.io versions, so we need to override them here to avoid +# version conflicts. +ark-ff = { git = "https://github.com/arkworks-rs/algebra" } +ark-ec = { git = "https://github.com/arkworks-rs/algebra" } +ark-serialize = { git = "https://github.com/arkworks-rs/algebra" } +ark-poly = { git = "https://github.com/arkworks-rs/algebra" } +ark-std = { git = "https://github.com/arkworks-rs/std" } +ark-crypto-primitives = { git = "https://github.com/winderica/crypto-primitives", rev = "af003fc" } +ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" } # "sw-fix-updated" branch + +# Curve crates also need git versions +ark-bn254 = { git = "https://github.com/arkworks-rs/algebra" } + +[patch."https://github.com/arkworks-rs/crypto-primitives"] +ark-crypto-primitives = { git = "https://github.com/winderica/crypto-primitives", rev = "af003fc" } + +[patch."https://github.com/arkworks-rs/r1cs-std"] +ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", rev = "ae8283a" } # "sw-fix-updated" branch \ No newline at end of file From 5e186c7d60b8f3c8155f08cf6cc18440879c60af Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 17 Feb 2026 23:05:06 +0800 Subject: [PATCH 29/93] Fix and test emulated integers with negative limbs --- .../primitives/src/algebra/field/emulated.rs | 259 +++++++++++++----- 1 file changed, 186 insertions(+), 73 deletions(-) diff --git a/crates/primitives/src/algebra/field/emulated.rs b/crates/primitives/src/algebra/field/emulated.rs index d75d61923..096a0d1ea 100644 --- a/crates/primitives/src/algebra/field/emulated.rs +++ b/crates/primitives/src/algebra/field/emulated.rs @@ -312,7 +312,7 @@ impl LimbedVar { // `d = 0` in this range (after we meet the first positive limb) // This guarantees that for every bit after the true bit in `helper`, // the corresponding limb in `delta` is zero. - (&r * &d).enforce_equal(&FpVar::zero())?; + r.mul_equals(&d, &FpVar::zero())?; // Add the current bit to `r`. r += FpVar::from(b); } @@ -494,8 +494,21 @@ impl LimbedVar Result<(), SynthesisError> { let len = min(self.limbs.len(), other.limbs.len()); - // Group the limbs of `self` and `other` so that each group nearly - // reaches the capacity `F::MODULUS_MINUS_ONE_DIV_TWO`. + let mut i = 0; + let mut carry = FpVar::zero(); + let mut x_bound = Bounds::zero(); + let mut y_bound = Bounds::zero(); + let mut step = 0; + // `unwrap` is safe as long as `F` is a prime field with `|F| > 2`. + let inv = F::from(BigUint::one() << F::BITS_PER_LIMB) + .inverse() + .unwrap(); + + // For each limb pair `(x_i, y_i)` in `self` and `other`, we first try + // to group their _bounds_ into `x_bound` and `y_bound`. + // If both new bounds do not overflow / underflow, we can safely group + // the _limbs_. + // // By saying group, we mean the operation `Σ x_i 2^{i * W}`, where `W` // is `F::BITS_PER_LIMB`, the initial number of bits in a limb. // This is just as what we do in grade school arithmetic, e.g., @@ -509,51 +522,58 @@ impl LimbedVar 2`. - let inv = F::from(BigUint::one() << F::BITS_PER_LIMB) - .inverse() - .unwrap(); - + // + // Assume a pair of grouped limb `(x', y')` consists of `k` original + // limbs. + // Then the lower `k * W` bits of `x'` and `y'` must be equal. + // To check that, we need to enforce that `2^{k * W}` divides `x' - y'`, + // which is done by computing the quotient `q = (x' - y') / 2^{k * W}` + // and enforcing `q` is small that doesn't cause the multiplication + // `q * 2^{k * W}` to overflow. + // + // Moreover, we need to take into account the carry from the previous + // grouped limb, i.e., we actually enforce `x' - y' + carry` is a + // multiple of `2^{k * W}`, and derive the next carry by computing the + // quotient `q`. + // + // We can further avoid storing `x'` and `y'` by updating the carry on + // the fly for each limb, i.e., `carry = (carry + x_i - y_i) / 2^W`. while i < len { if let (Some(new_x_bound), Some(new_y_bound)) = ( self.bounds[i].shl(step).add(&x_bound).filter_safe::(), other.bounds[i].shl(step).add(&y_bound).filter_safe::(), ) { - diff = (diff + &self.limbs[i] - &other.limbs[i]) * inv; - x_bound = new_x_bound; - y_bound = new_y_bound; + carry = (carry + &self.limbs[i] - &other.limbs[i]) * inv; + // The current limb pair is successfully grouped, so we move on + // to the next limb pair. i += 1; + + // Update the bounds and step for the current group. + x_bound = new_x_bound; + y_bound = new_y_bound; step += F::BITS_PER_LIMB; - continue; + } else { + // New bounds overflow / underflow, meaning the current group is + // finalized. + + // `bits` is the maximum possible bit-length of the carry's + // absolute value. + let bits = (max( + min(&x_bound.0, &y_bound.0).bits(), + max(&x_bound.1, &y_bound.1).bits(), + ) as usize) + .saturating_sub(step); + + // We ensure `carry` is small, i.e., `|carry| < 2^bits`, which + // guarantees that `carry * 2^{step}` does not overflow. + (&carry + F::from(BigUint::one() << bits)).enforce_bit_length(bits + 1)?; + + // Reset the bounds and step for the next group. + x_bound = Bounds::zero(); + y_bound = Bounds::zero(); + step = 0; } - // For each group, check the last `step_i` bits of `x_i` and `y_i` are - // equal. - // The intuition is to check `diff = x_i - y_i = 0 (mod 2^step_i)`. - // However, this is only true for `i = 0`, and we need to consider carry - // values `diff >> step_i` for `i > 0`. - // Therefore, we actually check `diff = x_i - y_i + c = 0 (mod 2^step_i)` - // and derive the next `c` by computing `diff >> step_i`. - // To enforce `diff = 0 (mod 2^step_i)`, we compute `diff / 2^step_i` - // and enforce it to be small (soundness holds because for `a` that does - // not divide `b`, `b / a` in the field will be very large). - let bits = (max( - min(&x_bound.0, &y_bound.0).bits(), - max(&x_bound.1, &y_bound.1).bits(), - ) as usize) - .saturating_sub(step); - - (&diff + F::from(BigUint::one() << bits)).enforce_bit_length(bits + 1)?; - - x_bound = Bounds::zero(); - y_bound = Bounds::zero(); - step = 0; } let remaining_limbs = if i < self.limbs.len() { @@ -567,29 +587,26 @@ impl LimbedVar>() - .enforce_equal(&FpVar::zero())?; - Bounds::add_many(remaining_bounds) + // Instead of doing that one by one, we check if their sum is zero + // using a single constraint. + // This is sound, as we first check that the bounds of their sum + // fit within the field capacity, which guarantees that the sum does + // not overflow or underflow, meaning that the sum is zero if and + // only if each limb is zero. + Bounds::add_many(&remaining_bounds[1..]) .filter_safe::() .ok_or(SynthesisError::Unsatisfiable)?; - // For the final carry, we need to ensure that it equals the - // remaining limb `rest`. - diff.enforce_equal(&remaining_limbs[0])?; + FpVar::zero().enforce_equal(&remaining_limbs[1..].iter().sum())?; } Ok(()) @@ -610,19 +627,23 @@ impl let cs = self.cs(); let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); // Provide the quotient and remainder as hints - let q = LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { - let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); - Ok(( - compose(self.limbs.value().unwrap_or_default()).div_floor(&m), - Bounds(lb, ub), - )) - })?; - let r = LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { - Ok(( - compose(self.limbs.value().unwrap_or_default()).abs() % &m, - Bounds(Zero::zero(), m.clone()), - )) - })?; + let (q, r) = { + let v = compose(self.limbs.value().unwrap_or_default()); + let q = v.div_floor(&m); + let r = v - &q * &m; + + ( + LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { + Ok(( + q, + Bounds(self.lbound().div_floor(&m), self.ubound().div_floor(&m)), + )) + })?, + LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { + Ok((r, Bounds(Zero::zero(), m.clone()))) + })?, + ) + }; let m = LimbedVar::constant(m); @@ -646,10 +667,11 @@ impl let m = BigInt::from_biguint(Sign::Plus, Target::MODULUS.into()); // Provide the quotient as hint let q = LimbedVar::new_variable_with_inferred_mode(cs.clone(), || { - let (lb, ub) = (self.lbound().div_floor(&m), self.ubound().div_floor(&m)); + let x = compose(self.limbs.value().unwrap_or_default()); + let y = compose(other.limbs.value().unwrap_or_default()); Ok(( - compose(self.limbs.value().unwrap_or_default()).div_floor(&m), - Bounds(lb, ub), + (x - y).div_floor(&m), + Bounds(self.lbound().div_floor(&m), self.ubound().div_floor(&m)), )) })?; @@ -1361,15 +1383,50 @@ mod tests { )) })?; + let neg_a_var = EmulatedFieldVar::constant(BigInt::zero()) - &a_var; + let neg_b_var = EmulatedFieldVar::constant(BigInt::zero()) - &b_var; + let neg_ab_var = EmulatedFieldVar::constant(BigInt::zero()) - &ab_var; + let neg_aab_var = EmulatedFieldVar::constant(BigInt::zero()) - &aab_var; + let neg_abb_var = EmulatedFieldVar::constant(BigInt::zero()) - &abb_var; + a_var .mul_unaligned(&b_var)? .enforce_equal_unaligned(&ab_var)?; + neg_a_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&ab_var)?; + a_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&neg_ab_var)?; + neg_a_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&neg_ab_var)?; + a_var .mul_unaligned(&ab_var)? .enforce_equal_unaligned(&aab_var)?; + neg_a_var + .mul_unaligned(&neg_ab_var)? + .enforce_equal_unaligned(&aab_var)?; + a_var + .mul_unaligned(&neg_ab_var)? + .enforce_equal_unaligned(&neg_aab_var)?; + neg_a_var + .mul_unaligned(&ab_var)? + .enforce_equal_unaligned(&neg_aab_var)?; + ab_var .mul_unaligned(&b_var)? .enforce_equal_unaligned(&abb_var)?; + neg_ab_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&abb_var)?; + ab_var + .mul_unaligned(&neg_b_var)? + .enforce_equal_unaligned(&neg_abb_var)?; + neg_ab_var + .mul_unaligned(&b_var)? + .enforce_equal_unaligned(&neg_abb_var)?; assert!(cs.is_satisfied()?); Ok(()) @@ -1392,9 +1449,65 @@ mod tests { let aab_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(aab))?; let abb_var = EmulatedFieldVar::new_witness(cs.clone(), || Ok(abb))?; + let neg_a_var = EmulatedFieldVar::constant(BigInt::zero()) - &a_var; + let neg_b_var = EmulatedFieldVar::constant(BigInt::zero()) - &b_var; + let neg_ab_var = EmulatedFieldVar::constant(BigInt::zero()) - &ab_var; + let neg_aab_var = EmulatedFieldVar::constant(BigInt::zero()) - &aab_var; + let neg_abb_var = EmulatedFieldVar::constant(BigInt::zero()) - &abb_var; + a_var.mul_unaligned(&b_var)?.enforce_congruent(&ab_var)?; + neg_a_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&ab_var)?; + a_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&neg_ab_var)?; + neg_a_var + .mul_unaligned(&b_var)? + .enforce_congruent(&neg_ab_var)?; + a_var.mul_unaligned(&ab_var)?.enforce_congruent(&aab_var)?; + neg_a_var + .mul_unaligned(&neg_ab_var)? + .enforce_congruent(&aab_var)?; + a_var + .mul_unaligned(&neg_ab_var)? + .enforce_congruent(&neg_aab_var)?; + neg_a_var + .mul_unaligned(&ab_var)? + .enforce_congruent(&neg_aab_var)?; + ab_var.mul_unaligned(&b_var)?.enforce_congruent(&abb_var)?; + neg_ab_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&abb_var)?; + ab_var + .mul_unaligned(&neg_b_var)? + .enforce_congruent(&neg_abb_var)?; + neg_ab_var + .mul_unaligned(&b_var)? + .enforce_congruent(&neg_abb_var)?; + + assert_eq!(a_var.mul_unaligned(&b_var)?.modulo()?.value()?, ab); + assert_eq!(neg_a_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, ab); + assert_eq!(a_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, -ab); + assert_eq!(neg_a_var.mul_unaligned(&b_var)?.modulo()?.value()?, -ab); + + assert_eq!(a_var.mul_unaligned(&ab_var)?.modulo()?.value()?, aab); + assert_eq!( + neg_a_var.mul_unaligned(&neg_ab_var)?.modulo()?.value()?, + aab + ); + assert_eq!(a_var.mul_unaligned(&neg_ab_var)?.modulo()?.value()?, -aab); + assert_eq!(neg_a_var.mul_unaligned(&ab_var)?.modulo()?.value()?, -aab); + + assert_eq!(ab_var.mul_unaligned(&b_var)?.modulo()?.value()?, abb); + assert_eq!( + neg_ab_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, + abb + ); + assert_eq!(ab_var.mul_unaligned(&neg_b_var)?.modulo()?.value()?, -abb); + assert_eq!(neg_ab_var.mul_unaligned(&b_var)?.modulo()?.value()?, -abb); assert!(cs.is_satisfied()?); Ok(()) From bd33ed00c28482b27b6a0206d3b4907e50a24ce0 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 30/93] Fix broken images in readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bb9ee6f10..19ca77b10 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Experimental folding schemes library implemented jointly by [0xPARC](https://0xparc.org/) and [PSE](https://pse.dev). - + Sonobe is a modular library to fold arithmetic circuit instances in an Incremental Verifiable computation (IVC) style. It features multiple folding schemes and decider setups, allowing users to pick the scheme which best fits their needs.

@@ -67,7 +67,7 @@ Once the IVC iterations are completed, the IVC proof is compressed into the Deci

- +

Where $w_i$ are the external witnesses used at each iterative step. @@ -87,14 +87,14 @@ The development flow using Sonobe looks like: 4. Generate the decider verifier

- +

The folding scheme and decider used can be swapped with a few lines of code (eg. switching from a Decider that uses two Spartan proofs over a cycle of curves, to a Decider that uses a single Groth16 proof over the BN254 to be verified in an Ethereum smart contract). The [Sonobe docs](https://privacy-scaling-explorations.github.io/sonobe-docs/) contain more details about the usage and design of the library. -Complete examples can be found at [folding-schemes/examples](https://github.com/privacy-scaling-explorations/sonobe/tree/main/examples) +Complete examples can be found at [folding-schemes/examples](https://github.com/privacy-scaling-explorations/sonobeAcknowledgments/tree/main/examples) ## License From 226f96157095d7c3e2bbdd7045cb7a1624575fa1 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 15:51:57 +0800 Subject: [PATCH 31/93] New CI --- .github/scripts/wasm-target-test-build.sh | 30 ---- .github/workflows/ci.yml | 161 ++++++---------------- 2 files changed, 39 insertions(+), 152 deletions(-) delete mode 100644 .github/scripts/wasm-target-test-build.sh diff --git a/.github/scripts/wasm-target-test-build.sh b/.github/scripts/wasm-target-test-build.sh deleted file mode 100644 index 3c42427cd..000000000 --- a/.github/scripts/wasm-target-test-build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -GIT_ROOT=$(pwd) - -cd /tmp - -# create test project -cargo new foobar -cd foobar - -# set rust-toolchain same as "sonobe" -cp "${GIT_ROOT}/rust-toolchain" . - -# add wasm32-* targets -rustup target add wasm32-unknown-unknown wasm32-wasip1 - -# add dependencies -cargo add --path "${GIT_ROOT}/frontends" --features wasm, parallel -cargo add --path "${GIT_ROOT}/folding-schemes" --features parallel -cargo add getrandom --features wasm_js --target wasm32-unknown-unknown - -# test build for wasm32-* targets -cargo build --release --target wasm32-unknown-unknown -cargo build --release --target wasm32-wasip1 -# Emscripten would require to fetch the `emcc` tooling. Hence we don't build the lib as a dep for it. -# cargo build --release --target wasm32-unknown-emscripten - -# delete test project -cd ../ -rm -rf foobar diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4b70d0cc..7989e8315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ name: CI Check on: + workflow_dispatch: merge_group: pull_request: push: @@ -36,44 +37,26 @@ concurrency: jobs: test: if: github.event.pull_request.draft == false - name: Test + name: Test (${{ matrix.features }}) runs-on: ubuntu-latest strategy: matrix: - feature_set: [basic] + features: + - default + - no-default include: - - feature_set: basic - features: --features default,light-test + - features: default + args: "" + - features: no-default + args: "--no-default-features" steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - - uses: noir-lang/noirup@v0.1.3 - with: - toolchain: 0.36.0 - - name: Download Circom - run: | - mkdir -p $HOME/bin - curl -sSfL https://github.com/iden3/circom/releases/download/v2.1.6/circom-linux-amd64 -o $HOME/bin/circom - chmod +x $HOME/bin/circom - echo "$HOME/bin" >> $GITHUB_PATH - - name: Download solc - run: | - curl -sSfL https://github.com/ethereum/solidity/releases/download/v0.8.4/solc-static-linux -o /usr/local/bin/solc - chmod +x /usr/local/bin/solc - - name: Execute compile.sh to generate .r1cs and .wasm from .circom - run: ./experimental-frontends/src/circom/test_folder/compile.sh - - name: Execute compile.sh to generate .json from noir - run: ./experimental-frontends/src/noir/test_folder/compile.sh + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --release --workspace --no-default-features ${{ matrix.features }} + run: cargo test --release --workspace ${{ matrix.args }} - name: Run Doc-tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --doc + run: cargo test --doc ${{ matrix.args }} build: if: github.event.pull_request.draft == false @@ -82,79 +65,21 @@ jobs: strategy: matrix: target: + - x86_64-unknown-linux-gnu - wasm32-unknown-unknown - wasm32-wasip1 - # Ignoring until clear usage is required - # - wasm32-unknown-emscripten - steps: - - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - override: false - default: true - - name: Add target - run: rustup target add ${{ matrix.target }} - - name: Wasm-compat experimental-frontends build - uses: actions-rs/cargo@v1 - with: - command: build - args: -p experimental-frontends --no-default-features --target ${{ matrix.target }} --features "wasm, parallel" - - name: Wasm-compat folding-schemes build - uses: actions-rs/cargo@v1 - with: - command: build - args: -p folding-schemes --no-default-features --target ${{ matrix.target }} --features "default,light-test" - - name: Run wasm-compat script - run: | - chmod +x .github/scripts/wasm-target-test-build.sh - .github/scripts/wasm-target-test-build.sh - shell: bash - - examples: - if: github.event.pull_request.draft == false - name: Run examples & examples tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - - uses: noir-lang/noirup@v0.1.3 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: 0.36.0 - - name: Download Circom - run: | - mkdir -p $HOME/bin - curl -sSfL https://github.com/iden3/circom/releases/download/v2.1.6/circom-linux-amd64 -o $HOME/bin/circom - chmod +x $HOME/bin/circom - echo "$HOME/bin" >> $GITHUB_PATH - - name: Download solc - run: | - curl -sSfL https://github.com/ethereum/solidity/releases/download/v0.8.4/solc-static-linux -o /usr/local/bin/solc - chmod +x /usr/local/bin/solc - - name: Execute compile.sh to generate .r1cs and .wasm from .circom - run: ./experimental-frontends/src/circom/test_folder/compile.sh - - name: Execute compile.sh to generate .json from noir - run: ./experimental-frontends/src/noir/test_folder/compile.sh - - name: Run examples tests - run: cargo test --examples - - name: Run examples - run: cargo run --release --example 2>&1 | grep -E '^ ' | xargs -n1 cargo run --release --example - - # run the benchmarks with the flag `--no-run` to ensure that they compile, - # but without executing them. - bench: - if: github.event.pull_request.draft == false - name: Bench compile - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/cargo@v1 - with: - command: bench - args: -p folding-schemes --no-run + - name: Build sonobe-primitives for ${{ matrix.target }} + run: cargo build -p sonobe-primitives --no-default-features --target ${{ matrix.target }} --features parallel + - name: Build sonobe-fs for ${{ matrix.target }} + run: cargo build -p sonobe-fs --no-default-features --target ${{ matrix.target }} --features parallel + - name: Build sonobe-ivc for ${{ matrix.target }} + run: cargo build -p sonobe-ivc --no-default-features --target ${{ matrix.target }} --features parallel fmt: if: github.event.pull_request.draft == false @@ -162,41 +87,33 @@ jobs: timeout-minutes: 30 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - - uses: Swatinem/rust-cache@v2 - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - command: fmt - args: --all --check + components: rustfmt + - uses: Swatinem/rust-cache@v2 + - name: Run rustfmt + run: cargo fmt --all --check clippy: if: github.event.pull_request.draft == false - name: Clippy lint checks + name: Clippy (${{ matrix.target }}) runs-on: ubuntu-latest strategy: matrix: - feature_set: [basic, wasm] - include: - - feature_set: basic - features: --features default - # We only want to test `experimental-frontends` package with `wasm` feature. - - feature_set: wasm - features: -p experimental-frontends --features wasm,parallel --target wasm32-unknown-unknown + target: + - x86_64-unknown-linux-gnu + - wasm32-unknown-unknown + - wasm32-wasip1 steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: components: clippy + targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - - name: Add target - run: rustup target add wasm32-unknown-unknown - name: Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --no-default-features ${{ matrix.features }} -- -D warnings + run: cargo clippy --workspace --all-targets --target ${{ matrix.target }} -- -D warnings typos: if: github.event.pull_request.draft == false From 9b7f4d66420f0975a75da76e8684ff4edb2ec61b Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 18:35:53 +0800 Subject: [PATCH 32/93] Actually test wasm targets --- .cargo/config.toml | 5 ++++ .github/workflows/ci.yml | 57 ++++++++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..983e89ba0 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.wasm32-unknown-unknown] +runner = 'wasm-bindgen-test-runner' + +[target.wasm32-wasip2] +runner = 'wasmtime' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7989e8315..9ca52f736 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,26 +37,45 @@ concurrency: jobs: test: if: github.event.pull_request.draft == false - name: Test (${{ matrix.features }}) + name: Test ${{ matrix.target }} (${{ matrix.features }}) runs-on: ubuntu-latest strategy: matrix: - features: - - default - - no-default include: - - features: default + # x64: both parallel and no-parallel + - target: x86_64-unknown-linux-gnu + features: parallel + args: "--features parallel" + - target: x86_64-unknown-linux-gnu + features: no-parallel + args: "" + # wasm: no-parallel only + - target: wasm32-unknown-unknown + features: no-parallel + args: "" + - target: wasm32-wasip2 + features: no-parallel args: "" - - features: no-default - args: "--no-default-features" steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - - name: Run tests - run: cargo test --release --workspace ${{ matrix.args }} - - name: Run Doc-tests - run: cargo test --doc ${{ matrix.args }} + - name: Install wasm-bindgen-cli + if: matrix.target == 'wasm32-unknown-unknown' + run: cargo install wasm-bindgen-cli + - name: Install wasmtime-cli + if: matrix.target == 'wasm32-wasip2' + run: cargo install wasmtime-cli + - name: Test sonobe-primitives + run: cargo test --release -p sonobe-primitives --target ${{ matrix.target }} ${{ matrix.args }} + - name: Test sonobe-fs + run: cargo test --release -p sonobe-fs --target ${{ matrix.target }} ${{ matrix.args }} + - name: Test sonobe-ivc + run: cargo test --release -p sonobe-ivc --target ${{ matrix.target }} ${{ matrix.args }} + - name: Test documentation examples + run: cargo test --doc --target ${{ matrix.target }} ${{ matrix.args }} build: if: github.event.pull_request.draft == false @@ -67,19 +86,19 @@ jobs: target: - x86_64-unknown-linux-gnu - wasm32-unknown-unknown - - wasm32-wasip1 + - wasm32-wasip2 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - - name: Build sonobe-primitives for ${{ matrix.target }} - run: cargo build -p sonobe-primitives --no-default-features --target ${{ matrix.target }} --features parallel - - name: Build sonobe-fs for ${{ matrix.target }} - run: cargo build -p sonobe-fs --no-default-features --target ${{ matrix.target }} --features parallel - - name: Build sonobe-ivc for ${{ matrix.target }} - run: cargo build -p sonobe-ivc --no-default-features --target ${{ matrix.target }} --features parallel + - name: Build sonobe-primitives + run: cargo build -p sonobe-primitives --target ${{ matrix.target }} + - name: Build sonobe-fs + run: cargo build -p sonobe-fs --target ${{ matrix.target }} + - name: Build sonobe-ivc + run: cargo build -p sonobe-ivc --target ${{ matrix.target }} fmt: if: github.event.pull_request.draft == false @@ -104,7 +123,7 @@ jobs: target: - x86_64-unknown-linux-gnu - wasm32-unknown-unknown - - wasm32-wasip1 + - wasm32-wasip2 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable From 44fa229ca691169864c097779a7c07bd72171c6d Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:11:22 +0800 Subject: [PATCH 33/93] Notes on terminology --- docs/Terminology.md | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/Terminology.md diff --git a/docs/Terminology.md b/docs/Terminology.md new file mode 100644 index 000000000..b6e736017 --- /dev/null +++ b/docs/Terminology.md @@ -0,0 +1,50 @@ +## Disambiguation of "Native" + +In cryptographic proof systems, the term "native" can have multiple interpretations depending on the specific context of discussion. + +### Context 1: Native vs Emulated + +When referring to "native field / curve" and "emulated (non-native) field / curve," "native" denotes that the field or curve can be directly represented within the arithmetic circuit of the proof system. +More specifically, "native" in native field means that the field is the same as the circuit's constraint field (i.e., the field over which the circuit is defined). +Similarly, a "native" curve is one whose base field (i.e., the field over which the curve is defined, which is also the field that a point's coordinates belong to) matches the circuit's constraint field. + +In contrast, "emulated" or "non-native" fields and curves are those that cannot be directly represented in the circuit's constraint field and thus require special handling (a.k.a. emulation) within the circuit. + +> [!TIP] +> A side note irrelevant to the main discussion is that the boundary between "native" and "emulated" is not very clear-cut. +> +> For instance, as long as the foundamental element of the circuit is not a curve point (which is the case for all current constraint systems), even a native curve is "emulated" in some sense because curve points need to be encoded as multiple elements in the constraint field and curve operations need to be broken down into field operations. +> One can further argue that, if we regard such an "emulation" as a native representation of the curve, then why not also consider emulated fields as native as well, since they are also encoded as multiple elements in the constraint field. +> +> In Sonobe, we distinguish "native" and "emulated" based on whether the in-circuit representation is the preferred or most efficient form for the given field or curve. For a field element, the preferred representation is a single element in the constraint field, while for a curve point, it is a tuple of elements in the constraint field representing the coordinates, but each coordinate itself is not further decomposed. Consequently, if the circuit is able to achieve these preferred representations, we classify the field or curve as "native"; otherwise, it is deemed "emulated." + +### Context 2: Native vs In-Circuit + +Another common usage of "native" is to distinguish between values and operations built in the host programming language (e.g., Rust) and those defined in the arithmetic circuit of the proof system. In the former case, we refer to them as "native"/"out-of-circuit", while in the latter case, we call them "in-circuit". + +### Proposed New Terminology + +It is unlikely for experienced practitioners to confuse the two contexts above when "native" is used, as the context usually makes it clear which meaning is intended. +However, to ensure everyone is on the same page and to avoid any potential mental overhead in interpreting "native" correctly, we propose adopting more specific terminology for each context. + +- For Context 1, prefer _Canonical_ vs _Emulated_. + + **Justification**: _Canonical_ is not a standard term in the literature and is coined for use in Sonobe. However, it intuitively conveys the idea of being the standard or preferred representation within the circuit. +- For Context 2: + - When referring to something that holds data: + - Prefer _Value_ vs _Variable_. Further qualify them as _Out-of-Circuit Value_ and _In-Circuit Variable_ if necessary. + - Neutral terms such as _Data_, _Element_, _Key_, _Instance_, _Witness_, etc., are also acceptable when solely focusing on the in-circuit or out-of-circuit context. + + **Justification**: The use of _Value_ and _Variable_ aligns with existing conventions, as these terms are widely used in the arkworks codebase. + - When referring to something that performs computation: + - Prefer _Widget_ vs _Gadget_. Further qualify them as _Out-of-Circuit Widget_ and _In-Circuit Gadget_ if necessary. + - Neutral terms such as _Algorithm_, _Procedure_, _Function_, _Method_, etc., are also acceptable when solely focusing on the in-circuit or out-of-circuit context. + + **Justification**: _Gadget_ is already a standard term for in-circuit computation modules or utilities. + + _Widget_ is invented by us to suggest a computational component that operates outside the circuit while maintaining a consistent and visually / phonetically appealing naming scheme. + + Furthermore, searching for "widget vs gadget" yields results that align with our intended meanings. For instance, [this article](https://www.thoughtco.com/widget-vs-gadget-3486689) suggests that in web development, "widgets work on multiple platforms, but gadgets are usually limited to specific devices or systems." This distinction resonates with our usage, where widgets operate in the general-purpose host environment, while gadgets are specialized for the circuit environment. + +The proposed terminology is used throughout the Sonobe documentation and codebase. +For contributions, we recommend doing so as well to enhance clarity and reduce ambiguity. However, in casual discussions / issue reports, it is fine to use "native" for both contexts. \ No newline at end of file From 7fdcd8617eb75462e1912ce311c17072aa2bac6f Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 04:39:50 +0800 Subject: [PATCH 34/93] Refactor: initialize revamped folding scheme impl --- Cargo.toml | 2 + crates/fs/Cargo.toml | 29 +++++ crates/fs/src/lib.rs | 281 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 crates/fs/Cargo.toml create mode 100644 crates/fs/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 916915046..94f8fa090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/primitives", + "crates/fs", ] resolver = "2" @@ -40,6 +41,7 @@ ark-vesta = { git = "https://github.com/arkworks-rs/algebra", default-features = # Local crates sonobe-primitives = { path = "crates/primitives", default-features = false } +sonobe-fs = { path = "crates/fs", default-features = false } [patch.crates-io] # We depend on git versions of arkworks crates, but some of our dependencies diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml new file mode 100644 index 000000000..093ca3777 --- /dev/null +++ b/crates/fs/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "sonobe-fs-wip" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } +ark-ec = { workspace = true } +ark-ff = { workspace = true, features = ["asm"] } +ark-poly = { workspace = true } +ark-relations = { workspace = true } +ark-std = { workspace = true, features = ["getrandom"] } +ark-serialize = { workspace = true } +thiserror = { workspace = true } +rayon = { workspace = true } + +sonobe-primitives = { workspace = true } + +[dev-dependencies] +ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } +ark-pallas = { workspace = true, features = ["curve", "r1cs"] } + +[features] +default = ["parallel"] +parallel = [ + "sonobe-primitives/parallel", +] diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs new file mode 100644 index 000000000..af008aedc --- /dev/null +++ b/crates/fs/src/lib.rs @@ -0,0 +1,281 @@ +use ark_relations::gr1cs::SynthesisError; +use ark_std::{fmt::Debug, rand::RngCore}; +use sonobe_primitives::{ + arithmetizations::Arith, + circuits::AssignmentsOwned, + commitments::CommitmentDef, + relations::{Relation, WitnessInstanceSampler}, + sumcheck::Error as SumCheckError, + traits::SonobeField, + transcripts::Transcript, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Arithmetization error: {0}")] + ArithError(#[from] sonobe_primitives::arithmetizations::Error), + #[error("Commitment error: {0}")] + CommitmentError(#[from] sonobe_primitives::commitments::Error), + #[error("Synthesis error: {0}")] + SynthesisError(#[from] SynthesisError), + #[error("Sumcheck error: {0}")] + SumCheckError(#[from] SumCheckError), + #[error("Unsupported use case: {0}")] + Unsupported(String), + #[error("Failed to create domain")] + DomainCreationFailure, +} + +pub trait FoldingWitness: Sync { + /// Returns the reference to all openings contained in the witness, each + /// being a tuple of the values being committed to and the randomness. + fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; +} + +impl FoldingWitness for Vec { + fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { + vec![] + } +} + +pub trait FoldingInstance: Debug + PartialEq + Sync { + /// Returns the commitments contained in the committed instance. + fn commitments(&self) -> Vec<&VC::Commitment>; +} + +impl FoldingInstance for Vec { + fn commitments(&self) -> Vec<&VC::Commitment> { + vec![] + } +} + +// pub trait WitnessOps: PartialEq + Clone + Debug { +// /// The in-circuit representation of the witness. +// type Var: AllocVar + WitnessVarOps; + +// /// Returns the openings (i.e., the values being committed to and the +// /// randomness) contained in the witness. +// fn get_openings(&self) -> Vec<(&[F], F)>; +// } + +// pub trait WitnessVarOps { +// /// Returns the openings (i.e., the values being committed to and the +// /// randomness) contained in the witness. +// fn get_openings(&self) -> Vec<(&[FpVar], FpVar)>; +// } + +// pub trait CommittedInstanceOps: Inputize + PartialEq + Clone + Debug { +// type C: Curve; + +// /// The in-circuit representation of the committed instance. +// type Var: AllocVar + CommittedInstanceVarOps; +// /// `hash` implements the committed instance hash compatible with the +// /// in-circuit implementation from `CommittedInstanceVarOps::hash`. +// /// +// /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and +// /// `U_i` is the committed instance `self`. +// fn hash>(&self, sponge: &T, i: F, z_0: &[F], z_i: &[F]) -> F +// where +// Self: Sized + Absorb, +// F: Absorb, +// { +// let mut sponge = sponge.clone(); +// sponge.absorb(&i); +// sponge.absorb(&z_0); +// sponge.absorb(&z_i); +// sponge.absorb(&self); +// sponge.squeeze_field_elements(1)[0] +// } + +// /// Returns the commitments contained in the committed instance. +// fn get_commitments(&self) -> Vec; + +// /// Returns `true` if the committed instance is an incoming instance, and +// /// `false` if it is a running instance. +// fn is_incoming(&self) -> bool; + +// /// Checks if the committed instance is an incoming instance. +// fn check_incoming(&self) -> Result<(), Error> { +// self.is_incoming() +// .then_some(()) +// .ok_or(Error::NotIncomingCommittedInstance) +// } +// } + +// pub trait CommittedInstanceVarOps { +// type PointVar; +// /// `hash` implements the in-circuit committed instance hash compatible with +// /// the native implementation from `CommittedInstanceOps::hash`. +// /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and +// /// `U_i` is the committed instance `self`. +// /// +// /// Additionally it returns the in-circuit representation of the committed +// /// instance `self` as a vector of field elements, so they can be reused in +// /// other gadgets avoiding recalculating (reconstraining) them. +// #[allow(clippy::type_complexity)] +// fn hash>( +// &self, +// sponge: &impl TranscriptVar, +// i: &FpVar, +// z_0: &[FpVar], +// z_i: &[FpVar], +// ) -> Result<(FpVar, Vec>), SynthesisError> +// where +// Self: AbsorbGadget, +// { +// let mut sponge = sponge.clone(); +// let vec = self.to_sponge_field_elements()?; +// sponge.absorb(&i)?; +// sponge.absorb(&z_0)?; +// sponge.absorb(&z_i)?; +// sponge.absorb(&vec)?; +// Ok(( +// // `unwrap` is safe because the sponge is guaranteed to return a single element +// sponge.squeeze_field_elements(1)?.pop().unwrap(), +// vec, +// )) +// } + +// /// Returns the commitments contained in the committed instance. +// fn get_commitments(&self) -> Vec; + +// /// Returns the public inputs contained in the committed instance. +// fn get_public_inputs(&self) -> &[FpVar]; + +// /// Generates constraints to enforce that the committed instance is an +// /// incoming instance. +// fn enforce_incoming(&self) -> Result<(), SynthesisError>; + +// /// Generates constraints to enforce that the committed instance `self` is +// /// partially equal to another committed instance `other`. +// /// Here, only field elements are compared, while commitments (points) are +// /// not. +// fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError>; +// } + +pub trait FoldingScheme { + type VC: CommitmentDef; + type RW: FoldingWitness; + type RU: FoldingInstance; + type IW: FoldingWitness; + type IU: FoldingInstance; + type TranscriptField: SonobeField; + type Arith: Arith; + type Config; + type PublicParam; + type ProverKey; + type VerifierKey; + type DeciderKey: Relation + + Relation + + WitnessInstanceSampler + + WitnessInstanceSampler< + Self::IW, + Self::IU, + Source = AssignmentsOwned<::Scalar>, + Error = Error, + >; + type Proof; + + /// The preprocessing method is a randomized algorithm that takes as input + /// the size bounds of the folding scheme, which are contained in the + /// `config` parameter, and outputs the public parameters. + /// + /// Here, the randomness source is controlled by `rng`. + /// + /// The security parameter is implicitly specified by the size of underlying + /// fields and groups. + fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; + + /// The key generation method is a deterministic algorithm that takes as + /// input the public parameters `pp` and the constraint system `arith`, and + /// outputs a prover key and a verifier key. + fn generate_keys( + pp: Self::PublicParam, + arith: Self::Arith, + ) -> Result<(Self::ProverKey, Self::VerifierKey, Self::DeciderKey), Error>; + + /// The proof generation method is a deterministic algorithm that takes as + /// input the prover key `pk`, the transcript `transcript` between the + /// prover and the verifier, the first witness-instance pair `W`, `U`, the + /// second witness-instance pair `w`, `u`, and outputs the folded witness + /// and instance, the proof, and the (intermediate) randomness. + /// + /// Here, the randomness source is controlled by `transcript`. The returned + /// intermediate randomness is useful for the construction of CycleFold + /// circuits in our CycleFold-based folding-to-IVC compiler. + fn prove( + pk: &Self::ProverKey, + transcript: &mut impl Transcript, + Ws: &[&Self::RW; M], + Us: &[&Self::RU; M], + ws: &[&Self::IW; N], + us: &[&Self::IU; N], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof), Error>; + + fn verify( + vk: &Self::VerifierKey, + transcript: &mut impl Transcript, + Us: &[&Self::RU; M], + us: &[&Self::IU; N], + proof: &Self::Proof, + ) -> Result; + + fn decide_running(dk: &Self::DeciderKey, W: &Self::RW, U: &Self::RU) -> Result<(), Error> { + Relation::::check_relation(dk, W, U) + } + + fn decide_incoming(dk: &Self::DeciderKey, w: &Self::IW, u: &Self::IU) -> Result<(), Error> { + Relation::::check_relation(dk, w, u) + } +} + +#[cfg(test)] +mod tests { + use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; + use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; + use ark_std::{error::Error, rand::Rng}; + use sonobe_primitives::{ + circuits::{ArithExtractor, AssignmentsOwned}, + transcripts::poseidon::poseidon_canonical_config, + }; + + use super::*; + + pub fn test_folding_scheme_1_1( + config: FS::Config, + circuit: impl ConstraintSynthesizer<::Scalar>, + assignments_vec: Vec::Scalar>>, + mut rng: impl Rng, + ) -> Result<(), Box> + where + FS: FoldingScheme<1, 1, Arith: From::Scalar>>>, + { + let pp = FS::preprocess(config, &mut rng)?; + + let cs = ArithExtractor::new(); + cs.execute_synthesizer(circuit)?; + let arith = cs.arith()?; + let (pk, vk, dk) = FS::generate_keys(pp, arith)?; + + let (mut W, mut U) = WitnessInstanceSampler::::sample(&dk, (), &mut rng)?; + FS::decide_running(&dk, &W, &U)?; + let mut transcript_p = PoseidonSponge::new(&poseidon_canonical_config()); + let mut transcript_v = PoseidonSponge::new(&poseidon_canonical_config()); + + for assignments in assignments_vec { + let (w, u) = + WitnessInstanceSampler::::sample(&dk, assignments, &mut rng)?; + FS::decide_incoming(&dk, &w, &u)?; + let (WW, UU, pi) = + FS::prove(&pk, &mut transcript_p, &[&W], &[&U], &[&w], &[&u], &mut rng)?; + FS::decide_running(&dk, &WW, &UU)?; + assert_eq!(FS::verify(&vk, &mut transcript_v, &[&U], &[&u], &pi)?, UU); + + (W, U) = (WW, UU); + } + + Ok(()) + } +} From 9982261d2aad61e5788dad4ab045c3ad9df1b362 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 15:04:52 +0800 Subject: [PATCH 35/93] Refactor: improve test template --- crates/fs/src/lib.rs | 68 +++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index af008aedc..625bfeda4 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -27,7 +27,7 @@ pub enum Error { DomainCreationFailure, } -pub trait FoldingWitness: Sync { +pub trait FoldingWitness: Debug + Sync { /// Returns the reference to all openings contained in the witness, each /// being a tuple of the values being committed to and the randomness. fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; @@ -207,18 +207,18 @@ pub trait FoldingScheme { fn prove( pk: &Self::ProverKey, transcript: &mut impl Transcript, - Ws: &[&Self::RW; M], - Us: &[&Self::RU; M], - ws: &[&Self::IW; N], - us: &[&Self::IU; N], + Ws: &[Self::RW; M], + Us: &[Self::RU; M], + ws: &[Self::IW; N], + us: &[Self::IU; N], rng: impl RngCore, ) -> Result<(Self::RW, Self::RU, Self::Proof), Error>; fn verify( vk: &Self::VerifierKey, transcript: &mut impl Transcript, - Us: &[&Self::RU; M], - us: &[&Self::IU; N], + Us: &[Self::RU; M], + us: &[Self::IU; N], proof: &Self::Proof, ) -> Result; @@ -243,14 +243,14 @@ mod tests { use super::*; - pub fn test_folding_scheme_1_1( + pub fn test_folding_scheme( config: FS::Config, circuit: impl ConstraintSynthesizer<::Scalar>, assignments_vec: Vec::Scalar>>, mut rng: impl Rng, ) -> Result<(), Box> where - FS: FoldingScheme<1, 1, Arith: From::Scalar>>>, + FS: FoldingScheme::Scalar>>>, { let pp = FS::preprocess(config, &mut rng)?; @@ -259,21 +259,51 @@ mod tests { let arith = cs.arith()?; let (pk, vk, dk) = FS::generate_keys(pp, arith)?; - let (mut W, mut U) = WitnessInstanceSampler::::sample(&dk, (), &mut rng)?; - FS::decide_running(&dk, &W, &U)?; + let mut Ws = vec![]; + let mut Us = vec![]; + for _ in 0..M { + let (W, U) = WitnessInstanceSampler::::sample(&dk, (), &mut rng)?; + FS::decide_running(&dk, &W, &U)?; + Ws.push(W); + Us.push(U); + } + let mut Ws = Ws.try_into().unwrap(); + let mut Us = Us.try_into().unwrap(); + let mut transcript_p = PoseidonSponge::new(&poseidon_canonical_config()); let mut transcript_v = PoseidonSponge::new(&poseidon_canonical_config()); for assignments in assignments_vec { - let (w, u) = - WitnessInstanceSampler::::sample(&dk, assignments, &mut rng)?; - FS::decide_incoming(&dk, &w, &u)?; - let (WW, UU, pi) = - FS::prove(&pk, &mut transcript_p, &[&W], &[&U], &[&w], &[&u], &mut rng)?; + let mut ws = vec![]; + let mut us = vec![]; + for _ in 0..N { + let (w, u) = WitnessInstanceSampler::::sample( + &dk, + assignments.clone(), + &mut rng, + )?; + FS::decide_incoming(&dk, &w, &u)?; + ws.push(w); + us.push(u); + } + let ws = ws.try_into().unwrap(); + let us = us.try_into().unwrap(); + + let (WW, UU, pi) = FS::prove(&pk, &mut transcript_p, &Ws, &Us, &ws, &us, &mut rng)?; FS::decide_running(&dk, &WW, &UU)?; - assert_eq!(FS::verify(&vk, &mut transcript_v, &[&U], &[&u], &pi)?, UU); - - (W, U) = (WW, UU); + assert_eq!(FS::verify(&vk, &mut transcript_v, &Us, &us, &pi)?, UU); + + for i in 0..M { + let (W, U) = WitnessInstanceSampler::::sample(&dk, (), &mut rng)?; + FS::decide_running(&dk, &W, &U)?; + Ws[i] = W; + Us[i] = U; + } + if M != 0 { + let idx = rng.gen_range(0..M); + Ws[idx] = WW; + Us[idx] = UU; + } } Ok(()) From c608afe9f2fd6343bd084c1d99213454cdc1ffe8 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 18:43:52 +0800 Subject: [PATCH 36/93] Refactor: initialize revamped IVC --- Cargo.toml | 2 + crates/fs/Cargo.toml | 2 +- crates/ivc/Cargo.toml | 22 +++++++++ crates/ivc/src/compilers/mod.rs | 0 crates/ivc/src/lib.rs | 84 +++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 crates/ivc/Cargo.toml create mode 100644 crates/ivc/src/compilers/mod.rs create mode 100644 crates/ivc/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 94f8fa090..d1b8c62ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/primitives", "crates/fs", + "crates/ivc", ] resolver = "2" @@ -42,6 +43,7 @@ ark-vesta = { git = "https://github.com/arkworks-rs/algebra", default-features = # Local crates sonobe-primitives = { path = "crates/primitives", default-features = false } sonobe-fs = { path = "crates/fs", default-features = false } +sonobe-ivc = { path = "crates/ivc", default-features = false } [patch.crates-io] # We depend on git versions of arkworks crates, but some of our dependencies diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 093ca3777..1ae15744f 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "sonobe-fs-wip" +name = "sonobe-fs" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml new file mode 100644 index 000000000..8d4d67c76 --- /dev/null +++ b/crates/ivc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "sonobe-ivc" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ark-ff = { workspace = true, features = ["asm"] } +ark-std = { workspace = true, features = ["getrandom"] } + +sonobe-primitives = { workspace = true } +sonobe-fs = { workspace = true } + +[dev-dependencies] + + +[features] +default = ["parallel"] +parallel = [ + "sonobe-fs/parallel", +] \ No newline at end of file diff --git a/crates/ivc/src/compilers/mod.rs b/crates/ivc/src/compilers/mod.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs new file mode 100644 index 000000000..6db1c46c3 --- /dev/null +++ b/crates/ivc/src/lib.rs @@ -0,0 +1,84 @@ +use ark_ff::PrimeField; +use ark_std::rand::RngCore; +use sonobe_primitives::circuits::FCircuit; + +pub mod compilers; + +pub enum Error {} + +pub trait IVC { + type Field: PrimeField; + + type Config; + type PublicParam; + type ProverKey; + type VerifierKey; + type Proof: Default; + + fn preprocess(rng: impl RngCore, config: &Self::Config) -> Result; + + fn generate_keys>( + pp: &Self::PublicParam, + step_circuit: &FC, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + + fn prove>( + pk: &Self::ProverKey, + step_circuit: &FC, + i: usize, + initial_state: &[Self::Field], + current_state: &[Self::Field], + external_inputs: FC::ExternalInputs, + current_proof: &Self::Proof, + ) -> Result<(Vec, Self::Proof), Error>; + + fn verify>( + vk: &Self::VerifierKey, + i: usize, + initial_state: &[Self::Field], + current_state: &[Self::Field], + proof: &Self::Proof, + ) -> Result<(), Error>; +} + +pub struct IVCStatefulProver { + pub pk: I::ProverKey, + pub step_circuit: FC, + pub i: usize, + pub initial_state: Vec, + pub current_state: Vec, + pub current_proof: I::Proof, +} + +impl, I: IVC> IVCStatefulProver { + pub fn new( + pk: I::ProverKey, + step_circuit: FC, + initial_state: Vec, + ) -> Result { + Ok(Self { + pk, + step_circuit, + i: 0, + current_state: initial_state.clone(), + initial_state, + current_proof: I::Proof::default(), + }) + } + + pub fn prove_step(&mut self, external_inputs: FC::ExternalInputs) -> Result<(), Error> { + let (next_state, next_proof) = I::prove( + &self.pk, + &self.step_circuit, + self.i, + &self.initial_state, + &self.current_state, + external_inputs, + &self.current_proof, + )?; + self.i += 1; + self.current_state = next_state; + self.current_proof = next_proof; + Ok(()) + } +} From b4f50d99a91f0f2007671ad50a3393aa2780c702 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 24 Oct 2025 20:49:56 +0800 Subject: [PATCH 37/93] Refactor: skeleton for cyclefold compiler --- crates/fs/Cargo.toml | 1 + crates/fs/src/lib.rs | 190 +++++++++------------- crates/ivc/Cargo.toml | 3 + crates/ivc/src/compilers/cyclefold/mod.rs | 105 ++++++++++++ crates/ivc/src/compilers/mod.rs | 1 + crates/ivc/src/lib.rs | 29 +++- 6 files changed, 206 insertions(+), 123 deletions(-) create mode 100644 crates/ivc/src/compilers/cyclefold/mod.rs diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 1ae15744f..6f0510f4b 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -10,6 +10,7 @@ ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", ark-ec = { workspace = true } ark-ff = { workspace = true, features = ["asm"] } ark-poly = { workspace = true } +ark-r1cs-std = { workspace = true } ark-relations = { workspace = true } ark-std = { workspace = true, features = ["getrandom"] } ark-serialize = { workspace = true } diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 625bfeda4..7bcd2ad49 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -1,13 +1,14 @@ +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar}; use ark_relations::gr1cs::SynthesisError; -use ark_std::{fmt::Debug, rand::RngCore}; +use ark_std::{borrow::Borrow, fmt::Debug, rand::RngCore}; use sonobe_primitives::{ arithmetizations::Arith, circuits::AssignmentsOwned, - commitments::CommitmentDef, + commitments::{CommitmentDef, CommitmentDefGadget}, relations::{Relation, WitnessInstanceSampler}, sumcheck::Error as SumCheckError, traits::SonobeField, - transcripts::Transcript, + transcripts::{Transcript, TranscriptGadget}, }; use thiserror::Error; @@ -50,110 +51,6 @@ impl FoldingInstance for Vec { } } -// pub trait WitnessOps: PartialEq + Clone + Debug { -// /// The in-circuit representation of the witness. -// type Var: AllocVar + WitnessVarOps; - -// /// Returns the openings (i.e., the values being committed to and the -// /// randomness) contained in the witness. -// fn get_openings(&self) -> Vec<(&[F], F)>; -// } - -// pub trait WitnessVarOps { -// /// Returns the openings (i.e., the values being committed to and the -// /// randomness) contained in the witness. -// fn get_openings(&self) -> Vec<(&[FpVar], FpVar)>; -// } - -// pub trait CommittedInstanceOps: Inputize + PartialEq + Clone + Debug { -// type C: Curve; - -// /// The in-circuit representation of the committed instance. -// type Var: AllocVar + CommittedInstanceVarOps; -// /// `hash` implements the committed instance hash compatible with the -// /// in-circuit implementation from `CommittedInstanceVarOps::hash`. -// /// -// /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and -// /// `U_i` is the committed instance `self`. -// fn hash>(&self, sponge: &T, i: F, z_0: &[F], z_i: &[F]) -> F -// where -// Self: Sized + Absorb, -// F: Absorb, -// { -// let mut sponge = sponge.clone(); -// sponge.absorb(&i); -// sponge.absorb(&z_0); -// sponge.absorb(&z_i); -// sponge.absorb(&self); -// sponge.squeeze_field_elements(1)[0] -// } - -// /// Returns the commitments contained in the committed instance. -// fn get_commitments(&self) -> Vec; - -// /// Returns `true` if the committed instance is an incoming instance, and -// /// `false` if it is a running instance. -// fn is_incoming(&self) -> bool; - -// /// Checks if the committed instance is an incoming instance. -// fn check_incoming(&self) -> Result<(), Error> { -// self.is_incoming() -// .then_some(()) -// .ok_or(Error::NotIncomingCommittedInstance) -// } -// } - -// pub trait CommittedInstanceVarOps { -// type PointVar; -// /// `hash` implements the in-circuit committed instance hash compatible with -// /// the native implementation from `CommittedInstanceOps::hash`. -// /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and -// /// `U_i` is the committed instance `self`. -// /// -// /// Additionally it returns the in-circuit representation of the committed -// /// instance `self` as a vector of field elements, so they can be reused in -// /// other gadgets avoiding recalculating (reconstraining) them. -// #[allow(clippy::type_complexity)] -// fn hash>( -// &self, -// sponge: &impl TranscriptVar, -// i: &FpVar, -// z_0: &[FpVar], -// z_i: &[FpVar], -// ) -> Result<(FpVar, Vec>), SynthesisError> -// where -// Self: AbsorbGadget, -// { -// let mut sponge = sponge.clone(); -// let vec = self.to_sponge_field_elements()?; -// sponge.absorb(&i)?; -// sponge.absorb(&z_0)?; -// sponge.absorb(&z_i)?; -// sponge.absorb(&vec)?; -// Ok(( -// // `unwrap` is safe because the sponge is guaranteed to return a single element -// sponge.squeeze_field_elements(1)?.pop().unwrap(), -// vec, -// )) -// } - -// /// Returns the commitments contained in the committed instance. -// fn get_commitments(&self) -> Vec; - -// /// Returns the public inputs contained in the committed instance. -// fn get_public_inputs(&self) -> &[FpVar]; - -// /// Generates constraints to enforce that the committed instance is an -// /// incoming instance. -// fn enforce_incoming(&self) -> Result<(), SynthesisError>; - -// /// Generates constraints to enforce that the committed instance `self` is -// /// partially equal to another committed instance `other`. -// /// Here, only field elements are compared, while commitments (points) are -// /// not. -// fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError>; -// } - pub trait FoldingScheme { type VC: CommitmentDef; type RW: FoldingWitness; @@ -175,6 +72,7 @@ pub trait FoldingScheme { Source = AssignmentsOwned<::Scalar>, Error = Error, >; + type Challenge; type Proof; /// The preprocessing method is a randomized algorithm that takes as input @@ -207,18 +105,18 @@ pub trait FoldingScheme { fn prove( pk: &Self::ProverKey, transcript: &mut impl Transcript, - Ws: &[Self::RW; M], - Us: &[Self::RU; M], - ws: &[Self::IW; N], - us: &[Self::IU; N], + Ws: &[impl Borrow; M], + Us: &[impl Borrow; M], + ws: &[impl Borrow; N], + us: &[impl Borrow; N], rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof), Error>; + ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; fn verify( vk: &Self::VerifierKey, transcript: &mut impl Transcript, - Us: &[Self::RU; M], - us: &[Self::IU; N], + Us: &[impl Borrow; M], + us: &[impl Borrow; N], proof: &Self::Proof, ) -> Result; @@ -231,6 +129,65 @@ pub trait FoldingScheme { } } +pub trait FoldingWitnessVar { + type Native: FoldingWitness; +} + +pub trait FoldingInstanceVar { + type Native: FoldingInstance; +} + +impl FoldingWitnessVar for Vec { + type Native = Vec<::Scalar>; +} + +impl FoldingInstanceVar for Vec { + type Native = Vec<::Scalar>; +} + +pub trait FoldingSchemePartialGadget { + type Native: FoldingScheme; + + type VC: CommitmentDefGadget; + type RW: FoldingWitnessVar>::RW>; + type RU: FoldingInstanceVar>::RU>; + // + AllocVar<>::RU, Self::TranscriptField>; + type IW: FoldingWitnessVar>::IW>; + type IU: FoldingInstanceVar>::IU>; + // + AllocVar<>::RU, Self::TranscriptField>; + + type TranscriptField: SonobeField; + + type VerifierKey; + + type Challenge; + + type Proof; + + type Hint; + + fn verify_hinted( + vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + Us: &[Self::RU; M], + us: &[Self::IU; N], + proof: &Self::Proof, + hint: Self::Hint, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; +} + +pub trait FoldingSchemeFullGadget: + FoldingSchemePartialGadget +{ + fn verify( + vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + Us: &[Self::RU; M], + us: &[Self::IU; N], + proof: &Self::Proof, + ) -> Result; +} + #[cfg(test)] mod tests { use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; @@ -250,7 +207,8 @@ mod tests { mut rng: impl Rng, ) -> Result<(), Box> where - FS: FoldingScheme::Scalar>>>, + FS: FoldingScheme, + FS::Arith: From::Scalar>>, { let pp = FS::preprocess(config, &mut rng)?; @@ -289,7 +247,7 @@ mod tests { let ws = ws.try_into().unwrap(); let us = us.try_into().unwrap(); - let (WW, UU, pi) = FS::prove(&pk, &mut transcript_p, &Ws, &Us, &ws, &us, &mut rng)?; + let (WW, UU, pi, _) = FS::prove(&pk, &mut transcript_p, &Ws, &Us, &ws, &us, &mut rng)?; FS::decide_running(&dk, &WW, &UU)?; assert_eq!(FS::verify(&vk, &mut transcript_v, &Us, &us, &pi)?, UU); diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml index 8d4d67c76..96666532b 100644 --- a/crates/ivc/Cargo.toml +++ b/crates/ivc/Cargo.toml @@ -6,8 +6,11 @@ license.workspace = true repository.workspace = true [dependencies] +ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } ark-ff = { workspace = true, features = ["asm"] } +ark-relations = { workspace = true } ark-std = { workspace = true, features = ["getrandom"] } +thiserror = { workspace = true } sonobe-primitives = { workspace = true } sonobe-fs = { workspace = true } diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs new file mode 100644 index 000000000..6fbc89e6e --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -0,0 +1,105 @@ +use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; +use ark_relations::gr1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, +}; +use ark_std::{marker::PhantomData, rand::RngCore}; + +use sonobe_fs::FoldingScheme; +use sonobe_primitives::circuits::ConstraintSystemExt; +use sonobe_primitives::relations::WitnessInstanceSampler; +use sonobe_primitives::traits::SonobeField; +use sonobe_primitives::transcripts::Transcript; +use sonobe_primitives::{circuits::FCircuit, commitments::CommitmentDef}; + +use crate::IVC; + +pub struct CycleFoldBasedIVC { + _fs1: PhantomData, + _fs2: PhantomData, +} + +pub struct ProverKey { + poseidon_config: PoseidonConfig, + pp_hash: FC::Field, +} + +impl IVC for CycleFoldBasedIVC +where + FS1: FoldingScheme< + 1, + 1, + VC: CommitmentDef>::TranscriptField>, + >, + FS2: FoldingScheme<1, 1> +{ + type Field = ::Scalar; + + type Config = (FS1::Config, FS2::Config); + + type PublicParam = (FS1::PublicParam, FS2::PublicParam); + + type ProverKey = ( + FS1::ProverKey, + FS1::DeciderKey, + FS2::ProverKey, + FS2::DeciderKey, + ProverKey, + ); + + type VerifierKey = (); + + type Proof = (FS1::RW, FS1::RU, FS1::IW, FS1::IU, FS2::RW, FS2::RU); + + fn preprocess( + config: Self::Config, + mut rng: impl RngCore, + ) -> Result { + Ok(( + FS1::preprocess(config.0, &mut rng)?, + FS2::preprocess(config.1, &mut rng)?, + )) + } + + fn generate_keys>( + pp: &Self::PublicParam, + step_circuit: &FC, + ) -> Result<(Self::ProverKey, Self::VerifierKey), crate::Error> { + todo!() + } + + fn prove>( + (pk_fs1, dk_fs1, pk_fs2, dk_fs2, pk): &Self::ProverKey, + step_circuit: &FC, + i: usize, + initial_state: &[FC::Field], + current_state: &[FC::Field], + external_inputs: FC::ExternalInputs, + (W, U, w, u, cfW, cfU): &Self::Proof, + mut rng: impl RngCore, + ) -> Result<(Vec, Self::Proof), crate::Error> { + let poseidon = PoseidonSponge::new_with_pp_hash(&pk.poseidon_config, pk.pp_hash); + let sponge = poseidon.separate_domain("sponge".as_ref()); + let mut transcript = poseidon.separate_domain("transcript".as_ref()); + + let (WW, UU, proof, challenge) = + FS1::prove(pk_fs1, &mut transcript, &[W], &[U], &[w], &[u], &mut rng)?; + + if i == 0 { + } else { + } + + let cs = ConstraintSystem::::new_ref(); + + todo!() + } + + fn verify>( + vk: &Self::VerifierKey, + i: usize, + initial_state: &[FC::Field], + current_state: &[FC::Field], + proof: &Self::Proof, + ) -> Result<(), crate::Error> { + todo!() + } +} diff --git a/crates/ivc/src/compilers/mod.rs b/crates/ivc/src/compilers/mod.rs index e69de29bb..41e86f249 100644 --- a/crates/ivc/src/compilers/mod.rs +++ b/crates/ivc/src/compilers/mod.rs @@ -0,0 +1 @@ +pub mod cyclefold; diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index 6db1c46c3..51f424172 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -1,21 +1,29 @@ use ark_ff::PrimeField; use ark_std::rand::RngCore; +use thiserror::Error; + use sonobe_primitives::circuits::FCircuit; pub mod compilers; -pub enum Error {} +#[derive(Debug, Error)] +pub enum Error { + #[error("Arithmetization error: {0}")] + ArithError(#[from] sonobe_primitives::arithmetizations::Error), + #[error("Folding error: {0}")] + FoldingError(#[from] sonobe_fs::Error), +} pub trait IVC { type Field: PrimeField; type Config; type PublicParam; - type ProverKey; - type VerifierKey; - type Proof: Default; + type ProverKey; + type VerifierKey; + type Proof; - fn preprocess(rng: impl RngCore, config: &Self::Config) -> Result; + fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; fn generate_keys>( pp: &Self::PublicParam, @@ -30,6 +38,7 @@ pub trait IVC { current_state: &[Self::Field], external_inputs: FC::ExternalInputs, current_proof: &Self::Proof, + rng: impl RngCore, ) -> Result<(Vec, Self::Proof), Error>; fn verify>( @@ -55,6 +64,7 @@ impl, I: IVC> IVCStatefulProver { pk: I::ProverKey, step_circuit: FC, initial_state: Vec, + initial_proof: I::Proof, ) -> Result { Ok(Self { pk, @@ -62,11 +72,15 @@ impl, I: IVC> IVCStatefulProver { i: 0, current_state: initial_state.clone(), initial_state, - current_proof: I::Proof::default(), + current_proof: initial_proof, }) } - pub fn prove_step(&mut self, external_inputs: FC::ExternalInputs) -> Result<(), Error> { + pub fn prove_step( + &mut self, + external_inputs: FC::ExternalInputs, + rng: impl RngCore, + ) -> Result<(), Error> { let (next_state, next_proof) = I::prove( &self.pk, &self.step_circuit, @@ -75,6 +89,7 @@ impl, I: IVC> IVCStatefulProver { &self.current_state, external_inputs, &self.current_proof, + rng, )?; self.i += 1; self.current_state = next_state; From 35b0459e678f22a89bcc88d3efb33be370bd3c1c Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 24 Oct 2025 22:38:47 +0800 Subject: [PATCH 38/93] Refactor: dedicated types for plain instance & witness --- crates/fs/src/lib.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 7bcd2ad49..97df6f0ae 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -34,18 +34,21 @@ pub trait FoldingWitness: Debug + Sync { fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; } -impl FoldingWitness for Vec { - fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { - vec![] - } -} - pub trait FoldingInstance: Debug + PartialEq + Sync { /// Returns the commitments contained in the committed instance. fn commitments(&self) -> Vec<&VC::Commitment>; } -impl FoldingInstance for Vec { +pub type PlainWitness = Vec<::Scalar>; +pub type PlainInstance = Vec<::Scalar>; + +impl FoldingWitness for PlainWitness { + fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { + vec![] + } +} + +impl FoldingInstance for PlainInstance { fn commitments(&self) -> Vec<&VC::Commitment> { vec![] } From 9383495b7f3b75373cb9dc67445897c4fb3150c9 Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 25 Oct 2025 23:27:13 +0800 Subject: [PATCH 39/93] Initialize augmented circuit --- crates/fs/src/lib.rs | 83 ++++++++++++++++--- .../ivc/src/compilers/cyclefold/circuits.rs | 66 +++++++++++++++ crates/ivc/src/compilers/cyclefold/mod.rs | 14 ++-- crates/ivc/src/lib.rs | 25 +++--- 4 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 crates/ivc/src/compilers/cyclefold/circuits.rs diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 97df6f0ae..107abcb20 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -1,5 +1,11 @@ -use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar}; -use ark_relations::gr1cs::SynthesisError; +use std::ops::{Deref, DerefMut}; + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, +}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::{borrow::Borrow, fmt::Debug, rand::RngCore}; use sonobe_primitives::{ arithmetizations::Arith, @@ -8,7 +14,7 @@ use sonobe_primitives::{ relations::{Relation, WitnessInstanceSampler}, sumcheck::Error as SumCheckError, traits::SonobeField, - transcripts::{Transcript, TranscriptGadget}, + transcripts::{Absorbable, AbsorbableVar, Transcript, TranscriptGadget}, }; use thiserror::Error; @@ -39,8 +45,44 @@ pub trait FoldingInstance: Debug + PartialEq + Sync { fn commitments(&self) -> Vec<&VC::Commitment>; } -pub type PlainWitness = Vec<::Scalar>; -pub type PlainInstance = Vec<::Scalar>; +#[derive(Debug, PartialEq)] +pub struct WrappedVec(Vec); + +impl Deref for WrappedVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WrappedVec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for WrappedVec { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl Absorbable for WrappedVec { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest) + } +} + +impl> AbsorbableVar for WrappedVec { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.0.absorb_into(dest) + } +} + +pub type PlainWitness = WrappedVec<::Scalar>; + +pub type PlainInstance = WrappedVec<::Scalar>; impl FoldingWitness for PlainWitness { fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { @@ -132,20 +174,39 @@ pub trait FoldingScheme { } } -pub trait FoldingWitnessVar { +pub trait FoldingWitnessVar: + AllocVar +{ type Native: FoldingWitness; } -pub trait FoldingInstanceVar { +pub trait FoldingInstanceVar: + AllocVar +{ type Native: FoldingInstance; } -impl FoldingWitnessVar for Vec { - type Native = Vec<::Scalar>; +pub type PlainWitnessVar = WrappedVec<::ScalarVar>; +pub type PlainInstanceVar = WrappedVec<::ScalarVar>; + +impl FoldingWitnessVar for PlainWitnessVar { + type Native = PlainWitness; +} + +impl FoldingInstanceVar for PlainInstanceVar { + type Native = PlainInstance; } -impl FoldingInstanceVar for Vec { - type Native = Vec<::Scalar>; +impl, Y, F: Field> AllocVar, F> for WrappedVec { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let v = f()?; + let v = v.borrow(); + Vec::new_variable(cs, || Ok(&v[..]), mode).map(|v| Self(v)) + } } pub trait FoldingSchemePartialGadget { diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs new file mode 100644 index 000000000..d0f589382 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -0,0 +1,66 @@ +use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; +use ark_ff::Zero; +use ark_relations::gr1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, +}; +use ark_std::{marker::PhantomData, rand::RngCore}; + +use sonobe_fs::{FoldingScheme, FoldingSchemeFullGadget, FoldingSchemePartialGadget}; +use sonobe_primitives::circuits::ConstraintSystemExt; +use sonobe_primitives::relations::WitnessInstanceSampler; +use sonobe_primitives::traits::SonobeField; +use sonobe_primitives::transcripts::Transcript; +use sonobe_primitives::{circuits::FCircuit, commitments::CommitmentDef}; + +pub struct AugmentedCircuit<'a, FC: FCircuit, FS1, FS2> { + poseidon_config: PoseidonConfig, + step_circuit: &'a FC, + _fs1: PhantomData, + _fs2: PhantomData, +} + +impl<'a, FC: FCircuit, FS1, FS2> AugmentedCircuit<'a, FC, FS1, FS2> +where + FS1: FoldingSchemePartialGadget<1, 1>, + FS2: FoldingSchemeFullGadget<1, 1>, +{ + fn compute_next_state( + &self, + cs: ConstraintSystemRef, + pp_hash: FC::Field, + i: usize, + initial_state: &FC::State, + current_state: &FC::State, + external_inputs: FC::ExternalInputs, + U: FS1::RU, + u: FS1::IU, + hint: FS1::Hint, + ) -> Result<(FC::State, FC::ExternalOutputs), SynthesisError> { + todo!() + } +} + +impl<'a, FC: FCircuit, FS1, FS2> ConstraintSynthesizer + for AugmentedCircuit<'a, FC, FS1, FS2> +where + FS1: FoldingSchemePartialGadget<1, 1>, + FS2: FoldingSchemeFullGadget<1, 1>, +{ + fn generate_constraints( + self, + cs: ConstraintSystemRef, + ) -> Result<(), SynthesisError> { + self.compute_next_state( + cs, + Default::default(), + 0, + &self.step_circuit.dummy_state(), + &self.step_circuit.dummy_state(), + todo!(), + todo!(), + todo!(), + todo!(), + ) + .map(|_| ()) + } +} diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 6fbc89e6e..c6ae6f108 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -13,6 +13,8 @@ use sonobe_primitives::{circuits::FCircuit, commitments::CommitmentDef}; use crate::IVC; +mod circuits; + pub struct CycleFoldBasedIVC { _fs1: PhantomData, _fs2: PhantomData, @@ -30,7 +32,7 @@ where 1, VC: CommitmentDef>::TranscriptField>, >, - FS2: FoldingScheme<1, 1> + FS2: FoldingScheme<1, 1>, { type Field = ::Scalar; @@ -71,12 +73,12 @@ where (pk_fs1, dk_fs1, pk_fs2, dk_fs2, pk): &Self::ProverKey, step_circuit: &FC, i: usize, - initial_state: &[FC::Field], - current_state: &[FC::Field], + initial_state: &FC::State, + current_state: &FC::State, external_inputs: FC::ExternalInputs, (W, U, w, u, cfW, cfU): &Self::Proof, mut rng: impl RngCore, - ) -> Result<(Vec, Self::Proof), crate::Error> { + ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), crate::Error> { let poseidon = PoseidonSponge::new_with_pp_hash(&pk.poseidon_config, pk.pp_hash); let sponge = poseidon.separate_domain("sponge".as_ref()); let mut transcript = poseidon.separate_domain("transcript".as_ref()); @@ -96,8 +98,8 @@ where fn verify>( vk: &Self::VerifierKey, i: usize, - initial_state: &[FC::Field], - current_state: &[FC::Field], + initial_state: &FC::State, + current_state: &FC::State, proof: &Self::Proof, ) -> Result<(), crate::Error> { todo!() diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index 51f424172..2f04c7d07 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -1,8 +1,7 @@ use ark_ff::PrimeField; use ark_std::rand::RngCore; -use thiserror::Error; - use sonobe_primitives::circuits::FCircuit; +use thiserror::Error; pub mod compilers; @@ -34,18 +33,18 @@ pub trait IVC { pk: &Self::ProverKey, step_circuit: &FC, i: usize, - initial_state: &[Self::Field], - current_state: &[Self::Field], + initial_state: &FC::State, + current_state: &FC::State, external_inputs: FC::ExternalInputs, current_proof: &Self::Proof, rng: impl RngCore, - ) -> Result<(Vec, Self::Proof), Error>; + ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error>; fn verify>( vk: &Self::VerifierKey, i: usize, - initial_state: &[Self::Field], - current_state: &[Self::Field], + initial_state: &FC::State, + current_state: &FC::State, proof: &Self::Proof, ) -> Result<(), Error>; } @@ -54,8 +53,8 @@ pub struct IVCStatefulProver { pub pk: I::ProverKey, pub step_circuit: FC, pub i: usize, - pub initial_state: Vec, - pub current_state: Vec, + pub initial_state: FC::State, + pub current_state: FC::State, pub current_proof: I::Proof, } @@ -63,7 +62,7 @@ impl, I: IVC> IVCStatefulProver { pub fn new( pk: I::ProverKey, step_circuit: FC, - initial_state: Vec, + initial_state: FC::State, initial_proof: I::Proof, ) -> Result { Ok(Self { @@ -80,8 +79,8 @@ impl, I: IVC> IVCStatefulProver { &mut self, external_inputs: FC::ExternalInputs, rng: impl RngCore, - ) -> Result<(), Error> { - let (next_state, next_proof) = I::prove( + ) -> Result { + let (next_state, external_outputs, next_proof) = I::prove( &self.pk, &self.step_circuit, self.i, @@ -94,6 +93,6 @@ impl, I: IVC> IVCStatefulProver { self.i += 1; self.current_state = next_state; self.current_proof = next_proof; - Ok(()) + Ok(external_outputs) } } From 2e50af4fabc9a27cdc8989185023148e42f9a466 Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 27 Oct 2025 17:19:35 +0800 Subject: [PATCH 40/93] Fully implement augmented circuit for CycleFold --- crates/fs/src/lib.rs | 166 ++++++++++------ crates/ivc/Cargo.toml | 1 + .../ivc/src/compilers/cyclefold/circuits.rs | 182 ++++++++++++++++-- crates/ivc/src/compilers/cyclefold/mod.rs | 37 ++-- 4 files changed, 300 insertions(+), 86 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 107abcb20..7f538bfa4 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -2,10 +2,13 @@ use std::ops::{Deref, DerefMut}; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ + GR1CSVar, alloc::{AllocVar, AllocationMode}, fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, }; -use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{borrow::Borrow, fmt::Debug, rand::RngCore}; use sonobe_primitives::{ arithmetizations::Arith, @@ -13,7 +16,7 @@ use sonobe_primitives::{ commitments::{CommitmentDef, CommitmentDefGadget}, relations::{Relation, WitnessInstanceSampler}, sumcheck::Error as SumCheckError, - traits::SonobeField, + traits::{Dummy, SonobeField}, transcripts::{Absorbable, AbsorbableVar, Transcript, TranscriptGadget}, }; use thiserror::Error; @@ -34,18 +37,40 @@ pub enum Error { DomainCreationFailure, } -pub trait FoldingWitness: Debug + Sync { +pub trait FoldingWitness: Debug { /// Returns the reference to all openings contained in the witness, each /// being a tuple of the values being committed to and the randomness. fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; } -pub trait FoldingInstance: Debug + PartialEq + Sync { +pub trait FoldingInstance: Clone + Debug + PartialEq { /// Returns the commitments contained in the committed instance. fn commitments(&self) -> Vec<&VC::Commitment>; + + fn public_inputs(&self) -> &[VC::Scalar]; +} + +pub type PlainWitness = WrappedVec<::Scalar>; + +pub type PlainInstance = WrappedVec<::Scalar>; + +impl FoldingWitness for PlainWitness { + fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { + vec![] + } +} + +impl FoldingInstance for PlainInstance { + fn commitments(&self) -> Vec<&VC::Commitment> { + vec![] + } + + fn public_inputs(&self) -> &[::Scalar] { + self + } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct WrappedVec(Vec); impl Deref for WrappedVec { @@ -80,28 +105,54 @@ impl> AbsorbableVar for WrappedVec { } } -pub type PlainWitness = WrappedVec<::Scalar>; - -pub type PlainInstance = WrappedVec<::Scalar>; - -impl FoldingWitness for PlainWitness { - fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { - vec![] +impl, Y, F: Field> AllocVar, F> for WrappedVec { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let v = f()?; + Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(|v| Self(v)) } } -impl FoldingInstance for PlainInstance { - fn commitments(&self) -> Vec<&VC::Commitment> { - vec![] +impl> CondSelectGadget for WrappedVec { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.len() != false_value.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(WrappedVec( + true_value + .0 + .iter() + .zip(false_value.0.iter()) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + )) } } +impl> GR1CSVar for WrappedVec { + type Value = WrappedVec; + + fn cs(&self) -> ConstraintSystemRef { + self.0.cs() + } + + fn value(&self) -> Result { + self.0.value().map(WrappedVec) + } +} pub trait FoldingScheme { type VC: CommitmentDef; type RW: FoldingWitness; - type RU: FoldingInstance; + type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; type IW: FoldingWitness; - type IU: FoldingInstance; + type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; type TranscriptField: SonobeField; type Arith: Arith; type Config; @@ -118,7 +169,7 @@ pub trait FoldingScheme { Error = Error, >; type Challenge; - type Proof; + type Proof: Clone + for<'a> Dummy<&'a ::Config>; /// The preprocessing method is a randomized algorithm that takes as input /// the size bounds of the folding scheme, which are contained in the @@ -175,66 +226,73 @@ pub trait FoldingScheme { } pub trait FoldingWitnessVar: - AllocVar + AllocVar + + GR1CSVar> +{ +} + +impl FoldingWitnessVar for T where + T: AllocVar + + GR1CSVar> { - type Native: FoldingWitness; } pub trait FoldingInstanceVar: - AllocVar + AllocVar + + GR1CSVar> + + AbsorbableVar + + CondSelectGadget { - type Native: FoldingInstance; + /// Returns the commitments contained in the committed instance. + fn commitments(&self) -> Vec<&VC::CommitmentVar>; + + fn public_inputs(&self) -> &Vec; } pub type PlainWitnessVar = WrappedVec<::ScalarVar>; pub type PlainInstanceVar = WrappedVec<::ScalarVar>; -impl FoldingWitnessVar for PlainWitnessVar { - type Native = PlainWitness; -} - impl FoldingInstanceVar for PlainInstanceVar { - type Native = PlainInstance; -} + fn commitments(&self) -> Vec<&VC::CommitmentVar> { + vec![] + } -impl, Y, F: Field> AllocVar, F> for WrappedVec { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let v = f()?; - let v = v.borrow(); - Vec::new_variable(cs, || Ok(&v[..]), mode).map(|v| Self(v)) + fn public_inputs(&self) -> &Vec { + self } } pub trait FoldingSchemePartialGadget { - type Native: FoldingScheme; + type Native: FoldingScheme::Widget>; type VC: CommitmentDefGadget; - type RW: FoldingWitnessVar>::RW>; - type RU: FoldingInstanceVar>::RU>; - // + AllocVar<>::RU, Self::TranscriptField>; - type IW: FoldingWitnessVar>::IW>; - type IU: FoldingInstanceVar>::IU>; - // + AllocVar<>::RU, Self::TranscriptField>; - - type TranscriptField: SonobeField; + type RW: FoldingWitnessVar>::RW>; + type RU: FoldingInstanceVar>::RU>; + type IW: FoldingWitnessVar>::IW>; + type IU: FoldingInstanceVar>::IU>; type VerifierKey; type Challenge; - type Proof; + type Proof: AllocVar< + >::Proof, + ::ConstraintField, + > + GR1CSVar< + ::ConstraintField, + Value = >::Proof, + >; - type Hint; + type Hint: AllocVar< + ::ConstraintField>>::Value, + ::ConstraintField, + > + GR1CSVar<::ConstraintField, Value: Default>; fn verify_hinted( vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - Us: &[Self::RU; M], - us: &[Self::IU; N], + transcript: &mut impl TranscriptGadget<::ConstraintField>, + Us: &[impl Borrow; M], + us: &[impl Borrow; N], proof: &Self::Proof, hint: Self::Hint, ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; @@ -245,9 +303,9 @@ pub trait FoldingSchemeFullGadget: { fn verify( vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - Us: &[Self::RU; M], - us: &[Self::IU; N], + transcript: &mut impl TranscriptGadget<::ConstraintField>, + Us: &[impl Borrow; M], + us: &[impl Borrow; N], proof: &Self::Proof, ) -> Result; } diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml index 96666532b..5c27a0280 100644 --- a/crates/ivc/Cargo.toml +++ b/crates/ivc/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true [dependencies] ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } ark-ff = { workspace = true, features = ["asm"] } +ark-r1cs-std = { workspace = true } ark-relations = { workspace = true } ark-std = { workspace = true, features = ["getrandom"] } thiserror = { workspace = true } diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index d0f589382..997029228 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -1,19 +1,43 @@ -use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; +use ark_crypto_primitives::sponge::poseidon::{ + PoseidonConfig, PoseidonSponge, constraints::PoseidonSpongeVar, +}; use ark_ff::Zero; +use ark_r1cs_std::{ + GR1CSVar, + alloc::AllocVar, + eq::EqGadget, + fields::{FieldVar, fp::FpVar}, +}; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, }; use ark_std::{marker::PhantomData, rand::RngCore}; +use sonobe_fs::{ + FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemeFullGadget, + FoldingSchemePartialGadget, +}; +use sonobe_primitives::{ + arithmetizations::Arith, + circuits::{ConstraintSystemExt, FCircuit}, + commitments::{CommitmentDef, CommitmentDefGadget}, + relations::WitnessInstanceSampler, + traits::{Dummy, SonobeField}, + transcripts::{Transcript, TranscriptGadget}, +}; -use sonobe_fs::{FoldingScheme, FoldingSchemeFullGadget, FoldingSchemePartialGadget}; -use sonobe_primitives::circuits::ConstraintSystemExt; -use sonobe_primitives::relations::WitnessInstanceSampler; -use sonobe_primitives::traits::SonobeField; -use sonobe_primitives::transcripts::Transcript; -use sonobe_primitives::{circuits::FCircuit, commitments::CommitmentDef}; +use crate::compilers::cyclefold::FoldingSchemeCycleFoldGadget; -pub struct AugmentedCircuit<'a, FC: FCircuit, FS1, FS2> { +pub struct AugmentedCircuit< + 'a, + FC: FCircuit, + FS1: FoldingSchemePartialGadget<1, 1>, + FS2: FoldingSchemeFullGadget<1, 1>, +> { poseidon_config: PoseidonConfig, + arith1_config: <>::Arith as Arith>::Config, + arith2_config: <>::Arith as Arith>::Config, + vk1: FS1::VerifierKey, + vk2: FS2::VerifierKey, step_circuit: &'a FC, _fs1: PhantomData, _fs2: PhantomData, @@ -21,10 +45,19 @@ pub struct AugmentedCircuit<'a, FC: FCircuit, FS1, FS2> { impl<'a, FC: FCircuit, FS1, FS2> AugmentedCircuit<'a, FC, FS1, FS2> where - FS1: FoldingSchemePartialGadget<1, 1>, - FS2: FoldingSchemeFullGadget<1, 1>, + FS1: FoldingSchemeCycleFoldGadget< + 1, + 1, + VC: CommitmentDefGadget>, + CFScalarVar = ::ScalarVar, + >, + FS2: FoldingSchemeFullGadget< + 1, + 1, + VC: CommitmentDefGadget>, + >, { - fn compute_next_state( + pub fn compute_next_state( &self, cs: ConstraintSystemRef, pp_hash: FC::Field, @@ -32,34 +65,141 @@ where initial_state: &FC::State, current_state: &FC::State, external_inputs: FC::ExternalInputs, - U: FS1::RU, - u: FS1::IU, - hint: FS1::Hint, + U: >::Value, + u: >::Value, + proof: >::Value, + hint: >::Value, + cf_U: >::Value, + cf_us: Vec<>::Value>, + cf_proofs: Vec<>::Value>, ) -> Result<(FC::State, FC::ExternalOutputs), SynthesisError> { - todo!() + let poseidon = PoseidonSpongeVar::new_with_pp_hash( + &self.poseidon_config, + &FpVar::new_witness(cs.clone(), || Ok(pp_hash))?, + )?; + let sponge = poseidon.separate_domain("sponge".as_ref())?; + let mut transcript = poseidon.separate_domain("transcript".as_ref())?; + + let i = FpVar::new_witness(cs.clone(), || Ok(FC::Field::from(i as u64)))?; + let ii = &i + FpVar::one(); + + let is_basecase = i.is_zero()?; + + let initial_state = FC::StateVar::new_witness(cs.clone(), || Ok(initial_state))?; + let current_state = FC::StateVar::new_witness(cs.clone(), || Ok(current_state))?; + + let U_dummy = FS1::RU::new_witness(cs.clone(), || { + Ok(>::Value::dummy(&self.arith1_config)) + })?; + let U = FS1::RU::new_witness(cs.clone(), || Ok(U))?; + let u = FS1::IU::new_witness(cs.clone(), || Ok(u))?; + let proof = FS1::Proof::new_witness(cs.clone(), || Ok(proof))?; + let hint = FS1::Hint::new_witness(cs.clone(), || Ok(hint))?; + + let cf_U_dummy = FS2::RU::new_witness(cs.clone(), || { + Ok(>::Value::dummy(&self.arith2_config)) + })?; + let cf_U = FS2::RU::new_witness(cs.clone(), || Ok(cf_U))?; + let cf_us = Vec::new_witness(cs.clone(), || Ok(cf_us))?; + let cf_proofs = Vec::new_witness(cs.clone(), || Ok(cf_proofs))?; + + let u_x = { + let mut sponge = sponge.clone(); + sponge.add(&i)?; + sponge.add(&initial_state)?; + sponge.add(¤t_state)?; + sponge.add(&U)?; + sponge.add(&cf_U)?; + sponge.get_field_elements(2)? + }; + + let (next_state, external_outputs) = + self.step_circuit + .generate_step_constraints(i, current_state, external_inputs)?; + + let (UU, rho) = FS1::verify_hinted(&self.vk1, &mut transcript, &[&U], &[&u], &proof, hint)?; + + let mut cf_UU = cf_U; + for (cf_u, cf_proof) in cf_us.iter().zip(&cf_proofs) { + cf_UU = FS2::verify(&self.vk2, &mut transcript, &[cf_UU], &[cf_u], cf_proof)?; + } + + let uu_x = { + let mut sponge = sponge.clone(); + sponge.add(&ii)?; + sponge.add(&initial_state)?; + sponge.add(&next_state)?; + sponge.add(&is_basecase.select(&U_dummy, &UU)?)?; + sponge.add(&is_basecase.select(&cf_U_dummy, &cf_UU)?)?; + sponge.get_field_elements(2)? + }; + // This line "converts" `uu_x` from witnesses to public inputs. + // Instead of directly modifying the constraint system, we explicitly + // allocate a public input and enforce that its value is indeed `uu_x`. + // While comparing `uu_x` with itself seems redundant, this is necessary + // because: + // - `.value()` allows an honest prover to extract public inputs without + // computing them outside the circuit. + // - `.enforce_equal()` prevents a malicious prover from claiming wrong + // public inputs that are not the honest `uu_x` computed in-circuit. + uu_x.enforce_equal(&Vec::new_input(cs.clone(), || uu_x.value())?)?; + + u.public_inputs().enforce_equal(&u_x)?; + + cf_us + .iter() + .zip(FS1::to_cyclefold_inputs(U, u, UU, rho)?) + .try_for_each(|(cf_u, cf_u_x)| cf_u.public_inputs().enforce_equal(&cf_u_x))?; + + if cs.is_in_setup_mode() { + Ok((self.step_circuit.dummy_state(), external_outputs)) + } else { + Ok((next_state.value()?, external_outputs)) + } } } impl<'a, FC: FCircuit, FS1, FS2> ConstraintSynthesizer for AugmentedCircuit<'a, FC, FS1, FS2> where - FS1: FoldingSchemePartialGadget<1, 1>, - FS2: FoldingSchemeFullGadget<1, 1>, + FS1: FoldingSchemeCycleFoldGadget< + 1, + 1, + VC: CommitmentDefGadget>, + CFScalarVar = ::ScalarVar, + >, + FS2: FoldingSchemeFullGadget< + 1, + 1, + VC: CommitmentDefGadget>, + >, { fn generate_constraints( self, cs: ConstraintSystemRef, ) -> Result<(), SynthesisError> { + let external_inputs = self.step_circuit.dummy_external_inputs(); + let U = >::Value::dummy(&self.arith1_config); + let u = Dummy::dummy(&self.arith1_config); + let proof = Dummy::dummy(&self.arith1_config); + let hint = Default::default(); + let cf_U = Dummy::dummy(&self.arith2_config); + let cf_us = vec![Dummy::dummy(&self.arith2_config); U.commitments().len()]; + let cf_proofs = vec![Dummy::dummy(&self.arith2_config); U.commitments().len()]; self.compute_next_state( cs, Default::default(), 0, &self.step_circuit.dummy_state(), &self.step_circuit.dummy_state(), - todo!(), - todo!(), - todo!(), - todo!(), + external_inputs, + U, + u, + proof, + hint, + cf_U, + cf_us, + cf_proofs, ) .map(|_| ()) } diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index c6ae6f108..1a538b2b0 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -1,20 +1,35 @@ use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; +use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, }; use ark_std::{marker::PhantomData, rand::RngCore}; - -use sonobe_fs::FoldingScheme; -use sonobe_primitives::circuits::ConstraintSystemExt; -use sonobe_primitives::relations::WitnessInstanceSampler; -use sonobe_primitives::traits::SonobeField; -use sonobe_primitives::transcripts::Transcript; -use sonobe_primitives::{circuits::FCircuit, commitments::CommitmentDef}; +use sonobe_fs::{FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemePartialGadget}; +use sonobe_primitives::{ + circuits::{ConstraintSystemExt, FCircuit}, + commitments::{CommitmentDef}, + relations::WitnessInstanceSampler, + traits::SonobeField, + transcripts::Transcript, +}; use crate::IVC; mod circuits; +pub trait FoldingSchemeCycleFoldGadget: + FoldingSchemePartialGadget +{ + type CFScalarVar; + + fn to_cyclefold_inputs( + U: Self::RU, + u: Self::IU, + UU: Self::RU, + rho: Self::Challenge, + ) -> Result>, SynthesisError>; +} + pub struct CycleFoldBasedIVC { _fs1: PhantomData, _fs2: PhantomData, @@ -28,10 +43,10 @@ pub struct ProverKey { impl IVC for CycleFoldBasedIVC where FS1: FoldingScheme< - 1, - 1, - VC: CommitmentDef>::TranscriptField>, - >, + 1, + 1, + VC: CommitmentDef>::TranscriptField>, + >, FS2: FoldingScheme<1, 1>, { type Field = ::Scalar; From 18b24b0bf34880c3ed8bd392acba042dba2c90ac Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 8 Nov 2025 08:47:34 +0800 Subject: [PATCH 41/93] Finish unified CycleFold compiler --- crates/fs/Cargo.toml | 1 + crates/fs/src/lib.rs | 230 ++++++++--- crates/ivc/Cargo.toml | 2 + .../ivc/src/compilers/cyclefold/circuits.rs | 381 ++++++++++++++---- crates/ivc/src/compilers/cyclefold/mod.rs | 335 ++++++++++++--- crates/ivc/src/lib.rs | 37 +- 6 files changed, 782 insertions(+), 204 deletions(-) diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 6f0510f4b..9d1860885 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -14,6 +14,7 @@ ark-r1cs-std = { workspace = true } ark-relations = { workspace = true } ark-std = { workspace = true, features = ["getrandom"] } ark-serialize = { workspace = true } +num-bigint = { workspace = true, features = ["rand"] } thiserror = { workspace = true } rayon = { workspace = true } diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 7f538bfa4..c8b728d8e 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -11,7 +11,7 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{borrow::Borrow, fmt::Debug, rand::RngCore}; use sonobe_primitives::{ - arithmetizations::Arith, + arithmetizations::{Arith, ArithConfig}, circuits::AssignmentsOwned, commitments::{CommitmentDef, CommitmentDefGadget}, relations::{Relation, WitnessInstanceSampler}, @@ -23,13 +23,13 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum Error { - #[error("Arithmetization error: {0}")] + #[error(transparent)] ArithError(#[from] sonobe_primitives::arithmetizations::Error), - #[error("Commitment error: {0}")] + #[error(transparent)] CommitmentError(#[from] sonobe_primitives::commitments::Error), - #[error("Synthesis error: {0}")] + #[error(transparent)] SynthesisError(#[from] SynthesisError), - #[error("Sumcheck error: {0}")] + #[error(transparent)] SumCheckError(#[from] SumCheckError), #[error("Unsupported use case: {0}")] Unsupported(String), @@ -38,42 +38,120 @@ pub enum Error { } pub trait FoldingWitness: Debug { + const N_OPENINGS: usize; + /// Returns the reference to all openings contained in the witness, each /// being a tuple of the values being committed to and the randomness. - fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; + fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; } -pub trait FoldingInstance: Clone + Debug + PartialEq { +pub trait FoldingInstance: Clone + Debug + PartialEq + Absorbable { + const N_COMMITMENTS: usize; + /// Returns the commitments contained in the committed instance. fn commitments(&self) -> Vec<&VC::Commitment>; fn public_inputs(&self) -> &[VC::Scalar]; + + fn public_inputs_mut(&mut self) -> &mut [VC::Scalar]; } -pub type PlainWitness = WrappedVec<::Scalar>; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PlainWitness(pub Vec); -pub type PlainInstance = WrappedVec<::Scalar>; +impl Deref for PlainWitness { + type Target = Vec; -impl FoldingWitness for PlainWitness { - fn openings_ref(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { - vec![] + fn deref(&self) -> &Self::Target { + &self.0 } } -impl FoldingInstance for PlainInstance { - fn commitments(&self) -> Vec<&VC::Commitment> { - vec![] +impl DerefMut for PlainWitness { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } +} - fn public_inputs(&self) -> &[::Scalar] { - self +impl From> for PlainWitness { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl Absorbable for PlainWitness { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest) + } +} + +impl> AbsorbableVar for PlainWitness { + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.0.absorb_into(dest) + } +} + +impl, Y, F: Field> AllocVar, F> for PlainWitness { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let v = f()?; + Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(|v| Self(v)) + } +} + +impl> CondSelectGadget for PlainWitness { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.len() != false_value.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self( + true_value + .0 + .iter() + .zip(false_value.0.iter()) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + )) } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct WrappedVec(Vec); +impl> GR1CSVar for PlainWitness { + type Value = PlainWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.0.cs() + } -impl Deref for WrappedVec { + fn value(&self) -> Result { + self.0.value().map(PlainWitness) + } +} + +impl Dummy<&A> for PlainWitness { + fn dummy(cfg: &A) -> Self { + vec![V::default(); cfg.n_witnesses()].into() + } +} + +impl FoldingWitness for PlainWitness { + const N_OPENINGS: usize = 0; + + fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { + vec![] + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PlainInstance(pub Vec); + +impl Deref for PlainInstance { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -81,32 +159,32 @@ impl Deref for WrappedVec { } } -impl DerefMut for WrappedVec { +impl DerefMut for PlainInstance { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl From> for WrappedVec { +impl From> for PlainInstance { fn from(v: Vec) -> Self { Self(v) } } -impl Absorbable for WrappedVec { +impl Absorbable for PlainInstance { fn absorb_into(&self, dest: &mut Vec) { self.0.absorb_into(dest) } } -impl> AbsorbableVar for WrappedVec { +impl> AbsorbableVar for PlainInstance { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.0.absorb_into(dest) } } -impl, Y, F: Field> AllocVar, F> for WrappedVec { - fn new_variable>>( +impl, Y, F: Field> AllocVar, F> for PlainInstance { + fn new_variable>>( cs: impl Into>, f: impl FnOnce() -> Result, mode: AllocationMode, @@ -116,7 +194,7 @@ impl, Y, F: Field> AllocVar, F> for WrappedVec> CondSelectGadget for WrappedVec { +impl> CondSelectGadget for PlainInstance { fn conditionally_select( cond: &Boolean, true_value: &Self, @@ -125,7 +203,7 @@ impl> CondSelectGadget for WrappedVec> CondSelectGadget for WrappedVec> GR1CSVar for WrappedVec { - type Value = WrappedVec; +impl> GR1CSVar for PlainInstance { + type Value = PlainInstance; fn cs(&self) -> ConstraintSystemRef { self.0.cs() } fn value(&self) -> Result { - self.0.value().map(WrappedVec) + self.0.value().map(PlainInstance) + } +} + +impl Dummy<&A> for PlainInstance { + fn dummy(cfg: &A) -> Self { + vec![V::default(); cfg.n_public_inputs()].into() } } + +impl FoldingWitness for PlainInstance { + const N_OPENINGS: usize = 0; + + fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { + vec![] + } +} + +impl FoldingInstance for PlainInstance { + const N_COMMITMENTS: usize = 0; + + fn commitments(&self) -> Vec<&VC::Commitment> { + vec![] + } + + fn public_inputs(&self) -> &[VC::Scalar] { + self + } + + fn public_inputs_mut(&mut self) -> &mut [VC::Scalar] { + self + } +} + +pub trait DeciderKey {} + pub trait FoldingScheme { type VC: CommitmentDef; - type RW: FoldingWitness; + type RW: FoldingWitness + for<'a> Dummy<&'a ::Config>; type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; - type IW: FoldingWitness; + type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; type TranscriptField: SonobeField; type Arith: Arith; @@ -159,7 +270,8 @@ pub trait FoldingScheme { type PublicParam; type ProverKey; type VerifierKey; - type DeciderKey: Relation + type DeciderKey: Clone + + Relation + Relation + WitnessInstanceSampler + WitnessInstanceSampler< @@ -198,6 +310,7 @@ pub trait FoldingScheme { /// Here, the randomness source is controlled by `transcript`. The returned /// intermediate randomness is useful for the construction of CycleFold /// circuits in our CycleFold-based folding-to-IVC compiler. + #[allow(non_snake_case)] fn prove( pk: &Self::ProverKey, transcript: &mut impl Transcript, @@ -208,6 +321,7 @@ pub trait FoldingScheme { rng: impl RngCore, ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; + #[allow(non_snake_case)] fn verify( vk: &Self::VerifierKey, transcript: &mut impl Transcript, @@ -216,6 +330,7 @@ pub trait FoldingScheme { proof: &Self::Proof, ) -> Result; + #[allow(non_snake_case)] fn decide_running(dk: &Self::DeciderKey, W: &Self::RW, U: &Self::RU) -> Result<(), Error> { Relation::::check_relation(dk, W, U) } @@ -247,10 +362,16 @@ pub trait FoldingInstanceVar: fn commitments(&self) -> Vec<&VC::CommitmentVar>; fn public_inputs(&self) -> &Vec; + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result; } -pub type PlainWitnessVar = WrappedVec<::ScalarVar>; -pub type PlainInstanceVar = WrappedVec<::ScalarVar>; +pub type PlainWitnessVar = PlainWitness<::ScalarVar>; +pub type PlainInstanceVar = PlainInstance<::ScalarVar>; impl FoldingInstanceVar for PlainInstanceVar { fn commitments(&self) -> Vec<&VC::CommitmentVar> { @@ -260,12 +381,20 @@ impl FoldingInstanceVar for PlainInstanceVar { fn public_inputs(&self) -> &Vec { self } + + fn new_witness_with_public_inputs( + _cs: impl Into>, + _u: &Self::Value, + x: Vec, + ) -> Result { + Ok(Self(x)) + } } pub trait FoldingSchemePartialGadget { - type Native: FoldingScheme::Widget>; + type Native: FoldingScheme; - type VC: CommitmentDefGadget; + type VC: CommitmentDefGadget>::VC>; type RW: FoldingWitnessVar>::RW>; type RU: FoldingInstanceVar>::RU>; type IW: FoldingWitnessVar>::IW>; @@ -283,24 +412,20 @@ pub trait FoldingSchemePartialGadget { Value = >::Proof, >; - type Hint: AllocVar< - ::ConstraintField>>::Value, - ::ConstraintField, - > + GR1CSVar<::ConstraintField, Value: Default>; - + #[allow(non_snake_case)] fn verify_hinted( vk: &Self::VerifierKey, transcript: &mut impl TranscriptGadget<::ConstraintField>, Us: &[impl Borrow; M], us: &[impl Borrow; N], proof: &Self::Proof, - hint: Self::Hint, ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; } pub trait FoldingSchemeFullGadget: FoldingSchemePartialGadget { + #[allow(non_snake_case)] fn verify( vk: &Self::VerifierKey, transcript: &mut impl TranscriptGadget<::ConstraintField>, @@ -314,22 +439,25 @@ pub trait FoldingSchemeFullGadget: mod tests { use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; - use ark_std::{error::Error, rand::Rng}; + use ark_std::{error::Error, rand::Rng, sync::Arc}; use sonobe_primitives::{ circuits::{ArithExtractor, AssignmentsOwned}, - transcripts::poseidon::poseidon_canonical_config, + transcripts::{ + griffin::{GriffinParams, sponge::GriffinSponge}, + poseidon::poseidon_canonical_config, + }, }; use super::*; - pub fn test_folding_scheme( + #[allow(non_snake_case)] + pub fn test_folding_scheme, const M: usize, const N: usize>( config: FS::Config, circuit: impl ConstraintSynthesizer<::Scalar>, assignments_vec: Vec::Scalar>>, mut rng: impl Rng, ) -> Result<(), Box> where - FS: FoldingScheme, FS::Arith: From::Scalar>>, { let pp = FS::preprocess(config, &mut rng)?; @@ -350,8 +478,10 @@ mod tests { let mut Ws = Ws.try_into().unwrap(); let mut Us = Us.try_into().unwrap(); - let mut transcript_p = PoseidonSponge::new(&poseidon_canonical_config()); - let mut transcript_v = PoseidonSponge::new(&poseidon_canonical_config()); + let config = Arc::new(GriffinParams::new(16, 5, 9)); + + let mut transcript_p = GriffinSponge::new(&config); + let mut transcript_v = GriffinSponge::new(&config); for assignments in assignments_vec { let mut ws = vec![]; diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml index 5c27a0280..be76625ee 100644 --- a/crates/ivc/Cargo.toml +++ b/crates/ivc/Cargo.toml @@ -17,6 +17,8 @@ sonobe-primitives = { workspace = true } sonobe-fs = { workspace = true } [dev-dependencies] +ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } +ark-grumpkin = { workspace = true, features = ["r1cs"] } [features] diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index 997029228..017036671 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -1,60 +1,62 @@ -use ark_crypto_primitives::sponge::poseidon::{ - PoseidonConfig, PoseidonSponge, constraints::PoseidonSpongeVar, -}; -use ark_ff::Zero; +use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, + boolean::Boolean, + convert::ToConstraintFieldGadget, eq::EqGadget, fields::{FieldVar, fp::FpVar}, + groups::CurveVar, }; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, }; -use ark_std::{marker::PhantomData, rand::RngCore}; +use ark_std::{marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_fs::{ FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemeFullGadget, FoldingSchemePartialGadget, }; use sonobe_primitives::{ + algebra::Val, arithmetizations::Arith, circuits::{ConstraintSystemExt, FCircuit}, commitments::{CommitmentDef, CommitmentDefGadget}, relations::WitnessInstanceSampler, - traits::{Dummy, SonobeField}, - transcripts::{Transcript, TranscriptGadget}, + traits::{CF1, CF2, Dummy, SonobeCurve, SonobeField}, + transcripts::{ + Transcript, TranscriptGadget, + griffin::{GriffinParams, sponge::GriffinSpongeVar}, + }, }; use crate::compilers::cyclefold::FoldingSchemeCycleFoldGadget; pub struct AugmentedCircuit< 'a, + FS1: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + FS2: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, FC: FCircuit, - FS1: FoldingSchemePartialGadget<1, 1>, - FS2: FoldingSchemeFullGadget<1, 1>, > { - poseidon_config: PoseidonConfig, - arith1_config: <>::Arith as Arith>::Config, - arith2_config: <>::Arith as Arith>::Config, - vk1: FS1::VerifierKey, - vk2: FS2::VerifierKey, - step_circuit: &'a FC, - _fs1: PhantomData, - _fs2: PhantomData, + pub griffin_config: Arc>, + pub arith1_config: &'a <>::Arith as Arith>::Config, + pub arith2_config: &'a <>::Arith as Arith>::Config, + pub step_circuit: &'a FC, } -impl<'a, FC: FCircuit, FS1, FS2> AugmentedCircuit<'a, FC, FS1, FS2> +impl<'a, FS1, FS2, FC: FCircuit> AugmentedCircuit<'a, FS1, FS2, FC> where FS1: FoldingSchemeCycleFoldGadget< 1, 1, + VerifierKey = (), VC: CommitmentDefGadget>, CFScalarVar = ::ScalarVar, >, FS2: FoldingSchemeFullGadget< 1, 1, - VC: CommitmentDefGadget>, + VerifierKey = (), + VC: CommitmentDefGadget, >, { pub fn compute_next_state( @@ -65,20 +67,19 @@ where initial_state: &FC::State, current_state: &FC::State, external_inputs: FC::ExternalInputs, - U: >::Value, - u: >::Value, + U: &>::Value, + u: &>::Value, proof: >::Value, - hint: >::Value, - cf_U: >::Value, + cf_U: &>::Value, cf_us: Vec<>::Value>, cf_proofs: Vec<>::Value>, ) -> Result<(FC::State, FC::ExternalOutputs), SynthesisError> { - let poseidon = PoseidonSpongeVar::new_with_pp_hash( - &self.poseidon_config, + let hash = GriffinSpongeVar::new_with_pp_hash( + &self.griffin_config, &FpVar::new_witness(cs.clone(), || Ok(pp_hash))?, )?; - let sponge = poseidon.separate_domain("sponge".as_ref())?; - let mut transcript = poseidon.separate_domain("transcript".as_ref())?; + let sponge = hash.separate_domain("sponge".as_ref())?; + let mut transcript = hash.separate_domain("transcript".as_ref())?; let i = FpVar::new_witness(cs.clone(), || Ok(FC::Field::from(i as u64)))?; let ii = &i + FpVar::one(); @@ -88,51 +89,63 @@ where let initial_state = FC::StateVar::new_witness(cs.clone(), || Ok(initial_state))?; let current_state = FC::StateVar::new_witness(cs.clone(), || Ok(current_state))?; - let U_dummy = FS1::RU::new_witness(cs.clone(), || { - Ok(>::Value::dummy(&self.arith1_config)) - })?; + let U_dummy = FS1::RU::new_constant( + cs.clone(), + >::Value::dummy(self.arith1_config), + )?; let U = FS1::RU::new_witness(cs.clone(), || Ok(U))?; - let u = FS1::IU::new_witness(cs.clone(), || Ok(u))?; let proof = FS1::Proof::new_witness(cs.clone(), || Ok(proof))?; - let hint = FS1::Hint::new_witness(cs.clone(), || Ok(hint))?; - let cf_U_dummy = FS2::RU::new_witness(cs.clone(), || { - Ok(>::Value::dummy(&self.arith2_config)) - })?; + let cf_U_dummy = FS2::RU::new_constant( + cs.clone(), + >::Value::dummy(self.arith2_config), + )?; let cf_U = FS2::RU::new_witness(cs.clone(), || Ok(cf_U))?; - let cf_us = Vec::new_witness(cs.clone(), || Ok(cf_us))?; let cf_proofs = Vec::new_witness(cs.clone(), || Ok(cf_proofs))?; - let u_x = { - let mut sponge = sponge.clone(); - sponge.add(&i)?; - sponge.add(&initial_state)?; - sponge.add(¤t_state)?; - sponge.add(&U)?; - sponge.add(&cf_U)?; - sponge.get_field_elements(2)? - }; + println!("{}", cs.num_constraints()); - let (next_state, external_outputs) = - self.step_circuit - .generate_step_constraints(i, current_state, external_inputs)?; + let u_x = sponge + .clone() + .add(&i)? + .add(&initial_state)? + .add(¤t_state)? + .add(&U)? + .add(&cf_U)? + .get_field_elements(2)?; + let u = FS1::IU::new_witness_with_public_inputs(cs.clone(), u, u_x)?; + let (UU, rho) = FS1::verify_hinted(&(), &mut transcript, &[&U], &[&u], &proof)?; + let actual_UU = is_basecase.select(&U_dummy, &UU)?; - let (UU, rho) = FS1::verify_hinted(&self.vk1, &mut transcript, &[&U], &[&u], &proof, hint)?; + println!("{}", cs.num_constraints()); let mut cf_UU = cf_U; - for (cf_u, cf_proof) in cf_us.iter().zip(&cf_proofs) { - cf_UU = FS2::verify(&self.vk2, &mut transcript, &[cf_UU], &[cf_u], cf_proof)?; + for ((cf_u, cf_u_x), cf_proof) in cf_us + .iter() + .zip(FS1::to_cyclefold_inputs(U, u, UU, proof, rho)?) + .zip(&cf_proofs) + { + let cf_u = FS2::IU::new_witness_with_public_inputs(cs.clone(), cf_u, cf_u_x)?; + cf_UU = FS2::verify(&(), &mut transcript, &[cf_UU], &[cf_u], cf_proof)?; } + let actual_cf_UU = is_basecase.select(&cf_U_dummy, &cf_UU)?; + + println!("{}", cs.num_constraints()); + + let (next_state, external_outputs) = + self.step_circuit + .generate_step_constraints(i, current_state, external_inputs)?; + + println!("{}", cs.num_constraints()); - let uu_x = { - let mut sponge = sponge.clone(); - sponge.add(&ii)?; - sponge.add(&initial_state)?; - sponge.add(&next_state)?; - sponge.add(&is_basecase.select(&U_dummy, &UU)?)?; - sponge.add(&is_basecase.select(&cf_U_dummy, &cf_UU)?)?; - sponge.get_field_elements(2)? - }; + let uu_x = sponge + .clone() + .add(&ii)? + .add(&initial_state)? + .add(&next_state)? + .add(&actual_UU)? + .add(&actual_cf_UU)? + .get_field_elements(2)?; // This line "converts" `uu_x` from witnesses to public inputs. // Instead of directly modifying the constraint system, we explicitly // allocate a public input and enforce that its value is indeed `uu_x`. @@ -142,14 +155,11 @@ where // computing them outside the circuit. // - `.enforce_equal()` prevents a malicious prover from claiming wrong // public inputs that are not the honest `uu_x` computed in-circuit. - uu_x.enforce_equal(&Vec::new_input(cs.clone(), || uu_x.value())?)?; + uu_x.enforce_equal(&Vec::new_input(cs.clone(), || { + Ok(uu_x.value().unwrap_or(vec![Default::default(); uu_x.len()])) + })?)?; - u.public_inputs().enforce_equal(&u_x)?; - - cf_us - .iter() - .zip(FS1::to_cyclefold_inputs(U, u, UU, rho)?) - .try_for_each(|(cf_u, cf_u_x)| cf_u.public_inputs().enforce_equal(&cf_u_x))?; + println!("{}", cs.num_constraints()); if cs.is_in_setup_mode() { Ok((self.step_circuit.dummy_state(), external_outputs)) @@ -159,48 +169,247 @@ where } } -impl<'a, FC: FCircuit, FS1, FS2> ConstraintSynthesizer - for AugmentedCircuit<'a, FC, FS1, FS2> +impl<'a, FS1, FS2, FC: FCircuit> ConstraintSynthesizer + for AugmentedCircuit<'a, FS1, FS2, FC> where FS1: FoldingSchemeCycleFoldGadget< 1, 1, + VerifierKey = (), VC: CommitmentDefGadget>, CFScalarVar = ::ScalarVar, >, FS2: FoldingSchemeFullGadget< 1, 1, - VC: CommitmentDefGadget>, + VerifierKey = (), + VC: CommitmentDefGadget, >, { fn generate_constraints( self, cs: ConstraintSystemRef, ) -> Result<(), SynthesisError> { - let external_inputs = self.step_circuit.dummy_external_inputs(); - let U = >::Value::dummy(&self.arith1_config); - let u = Dummy::dummy(&self.arith1_config); - let proof = Dummy::dummy(&self.arith1_config); - let hint = Default::default(); - let cf_U = Dummy::dummy(&self.arith2_config); - let cf_us = vec![Dummy::dummy(&self.arith2_config); U.commitments().len()]; - let cf_proofs = vec![Dummy::dummy(&self.arith2_config); U.commitments().len()]; self.compute_next_state( cs, Default::default(), 0, &self.step_circuit.dummy_state(), &self.step_circuit.dummy_state(), - external_inputs, - U, - u, - proof, - hint, - cf_U, - cf_us, - cf_proofs, + self.step_circuit.dummy_external_inputs(), + &Dummy::dummy(self.arith1_config), + &Dummy::dummy(self.arith1_config), + Dummy::dummy(self.arith1_config), + &Dummy::dummy(self.arith2_config), + vec![Dummy::dummy(self.arith2_config); >::Value::N_COMMITMENTS], + vec![Dummy::dummy(self.arith2_config); >::Value::N_COMMITMENTS], ) .map(|_| ()) } } + +/// [`CycleFoldConfig`] controls the behavior of [`CycleFoldCircuit`]. +/// +/// Looking ahead, the circuit computes the random linear combination of points, +/// which is essentially done by iteratively computing `P = (P + p_i) * r_i`, +/// where `P` is the folded point, `p_i` is the input point, and `r_i` is the +/// randomness. +pub trait CycleFoldConfig: Sized + Default { + type C: SonobeCurve; + + /// `N_INPUT_POINTS` specifies the number of input points that are folded in + /// [`CycleFoldCircuit`] via random linear combinations. + const N_INPUT_POINTS: usize; + /// `N_UNIQUE_RANDOMNESSES` specifies the number of *unique* randomnesses + /// allocated in [`CycleFoldCircuit`]. Although the linear combination in + /// general consists of multiple randomnesses, some folding schemes (such as + /// Nova and HyperNova) only need a single one. Thus, by setting this value, + /// the circuit can learn how many randomnesses are used and how long the + /// public inputs vector should be. + const N_UNIQUE_RANDOMNESSES: usize; + /// `RANDOMNESS_BIT_LENGTH` is the maximum bit length of a randomness `r_i`. + const RANDOMNESS_BIT_LENGTH: usize; + /// `FIELD_CAPACITY` is the maximum number of bits that can be stored in a + /// field element. + /// + /// By default, `FIELD_CAPACITY` is set to `MODULUS_BIT_SIZE - 1`. + /// + /// Given a randomness `r_i` with `RANDOMNESS_BIT_LENGTH` bits, we need + /// `RANDOMNESS_BIT_LENGTH / FIELD_CAPACITY` field elements to represent it + /// *compactly* in-circuit. + const FIELD_CAPACITY: usize = CF2::::MODULUS_BIT_SIZE as usize - 1; + + /// Public inputs length for the [`CycleFoldCircuit`], which depends on the + /// above constants defined by the concrete folding scheme. For example: + /// * In Nova, this is `|r| + |p_1| + |p_2| + |P|` + /// * In HyperNova, this is `|r| + |p_i| * n_points + |P|`. + /// * In ProtoGalaxy, this is `|[..., r_i, ...]| + |p_i| * n_points + |P|`. + /// + /// As explained above, `|r|` (i.e., the length of a single randomness) is + /// `RANDOMNESS_BIT_LENGTH / FIELD_CAPACITY`. + /// When there are multiple randomnesses, the length of `|[..., r_i, ...]|` + /// is `RANDOMNESS_BIT_LENGTH * N_UNIQUE_RANDOMNESSES / FIELD_CAPACITY`, as + /// the bits of all randomnesses are concatenated before being packed into + /// field elements. + /// The length of a point `p_i` when treated as public inputs is 2, as we + /// only need the `x` and `y` coordinates of the point. + /// + /// Thus, `IO_LEN` is `RANDOMNESS_BIT_LENGTH * N_UNIQUE_RANDOMNESSES / FIELD_CAPACITY + 2 * (N_INPUT_POINTS + 1)`. + const IO_LEN: usize = { + (Self::RANDOMNESS_BIT_LENGTH * Self::N_UNIQUE_RANDOMNESSES).div_ceil(Self::FIELD_CAPACITY) + + 2 * (Self::N_INPUT_POINTS + 1) + }; + + /// `alloc_points` allocates the points that are going to be folded in the + /// [`CycleFoldCircuit`] via random linear combinations. + /// + /// The implementation must allocate the points as *witness* variables (i.e. + /// by calling [`AllocVar::new_witness`]) first, then mark them as public + /// inputs by calling [`CycleFoldConfig::mark_point_as_public`], and finally + /// return the allocated witness variables. + /// + /// While it is possible to allocate the points as public inputs directly, + /// we do not use this approach because this will create a longer vector of + /// public inputs, which is not ideal for the augmented step circuit on the + /// primary curve. + fn alloc_points( + &self, + cs: ConstraintSystemRef>, + ) -> Result::Var>, SynthesisError>; + + /// `alloc_randomnesses` allocates the randomnesses used as coefficients of + /// the random linear combinations in the `CycleFoldCircuit`. + /// + /// The implementation must allocate the randomnesses as *witness* variables + /// (i.e. by calling [`AllocVar::new_witness`]) first, then mark them as + /// public inputs by calling [`CycleFoldConfig::mark_point_as_public`], and + /// finally return the allocated witness variables. + /// + /// See [`CycleFoldConfig::alloc_points`] for the reason why they need to be + /// allocated as witness variables first and converted to public later. + /// + /// In addition, because the circuit computes `P = (P + p_i) * r_i` for each + /// `i` from `N_INPUT_POINTS - 1` down to `0`, the actual linear combination + /// is `P = r_0 * p_0 + (r_0 r_1) * p_1 + (r_0 r_1 r_2) * p_2 + ...`. Thus, + /// to compute `P = R_0 p_0 + R_1 p_1 + R_2 p_2 + ...`, the implementation + /// should return `r_0 = R_0, r_1 = R_1 / R_0, ..., r_i = R_i / R_{i - 1}`. + /// A special case is `R_i = R^i`, where the allocated randomnesses become + /// `r_0 = 1, r_1 = r_2 = ... = R`. + fn alloc_randomnesses( + &self, + cs: ConstraintSystemRef>, + ) -> Result>>>, SynthesisError>; + + /// `mark_point_as_public` marks a point as public. + /// + /// The final vector of public inputs is shorter than the result of calling + /// [`AllocVar::new_input`], because we only need the x and y coordinates of + /// the point, but the `infinity` flag is not necessary. + fn mark_point_as_public(point: &::Var) -> Result<(), SynthesisError> { + for x in &point.to_constraint_field()?[..2] { + // This line "converts" `x` from a witness to a public input. + // Instead of directly modifying the constraint system, we explicitly + // allocate a public input and enforce that its value is indeed `x`. + // While comparing `x` with itself seems redundant, this is necessary + // because: + // - `.value()` allows an honest prover to extract public inputs without + // computing them outside the circuit. + // - `.enforce_equal()` prevents a malicious prover from claiming wrong + // public inputs that are not the honest `x` computed in-circuit. + FpVar::new_input(x.cs().clone(), || x.value())?.enforce_equal(x)?; + } + Ok(()) + } + + /// `mark_randomness_as_public` marks randomness as public. + /// + /// The final vector of public inputs is shorter than the result of calling + /// [`AllocVar::new_input`], because we pack the bits of randomness into + /// a compact field elements. + fn mark_randomness_as_public(r: &[Boolean>]) -> Result<(), SynthesisError> { + for bits in r.chunks(Self::FIELD_CAPACITY) { + let x = Boolean::le_bits_to_fp(bits)?; + FpVar::new_input(x.cs().clone(), || x.value())?.enforce_equal(&x)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, Default)] +pub struct CycleFoldCircuit { + _cfg: PhantomData, +} + +impl CycleFoldCircuit { + pub fn fold_points( + &self, + cs: ConstraintSystemRef>, + cfg: Cfg, + ) -> Result<(), SynthesisError> { + let rs = cfg.alloc_randomnesses(cs.clone())?; + let points = cfg.alloc_points(cs.clone())?; + + #[cfg(test)] + { + assert_eq!(Cfg::N_INPUT_POINTS, points.len()); + assert_eq!(Cfg::N_INPUT_POINTS, rs.len()); + for r in &rs { + assert_eq!(Cfg::RANDOMNESS_BIT_LENGTH, r.len()); + } + } + + // A slightly optimized version of `scalar_mul_le`. + fn point_mul( + point: &C::Var, + r: &[Boolean>], + ) -> Result { + if r.is_constant() { + let r = CF1::::from( as PrimeField>::BigInt::from_bits_le(&r.value()?)); + if r.is_one() { + return Ok(point.clone()); + } + } + point.scalar_mul_le(r.iter()) + } + + // Given a vector of points (over the primary curve) that are obtained + // from the instances of the folding scheme, we fold them *natively* in + // the CycleFold circuit (over the secondary curve). + // * In Nova, we need to compute P = p_0 + R * p_1. + // - for the cmW we're computing: U_i1.cmW = U_i.cmW + R * u_i.cmW + // - for the cmE we're computing: U_i1.cmE = U_i.cmE + R * cmT + R^2 * u_i.cmE, where u_i.cmE + // is assumed to be 0, so, U_i1.cmE = U_i.cmE + R * cmT + // * In HyperNova, we need to compute P = p_0 + R * p_1 + R^2 * p_2 + ... + R^{n-1} * p_{n-1}. + // * In ProtoGalaxy, we need to compute P = R_0 * p_0 + R_1 * p_1 + R_2 * p_2 + ... + R_{n-1} * p_{n-1}. + // + // To handle HyperNova more efficiently (with less constraints), we do + // P = ((((p_{n-1} * R) + p_{n-2}) * R + p_{n-3}) * R + ...) * R + p_0. + // This can be done iteratively by computing P = (P + p_i) * R. + // + // We further generalize this to support ProtoGalaxy, which now becomes + // P = (((((p_{n-1} * r_{n-1}) + p_{n-2}) * r_{n-2} + p_{n-3}) * r_{n-3} + ...) * r_1 + p_0) * r_0 + // + // Here, r_0 = 1, r_1 = r_2 = ... = r_{n-1} = R for Nova and HyperNova, + // and r_i = R_i / R_{i - 1} for ProtoGalaxy. + let mut p_folded = point_mul::( + &points[Cfg::N_INPUT_POINTS - 1], + &rs[Cfg::N_INPUT_POINTS - 1], + )?; + for i in (0..Cfg::N_INPUT_POINTS - 1).rev() { + p_folded = point_mul::(&(p_folded + &points[i]), &rs[i])?; + } + + Cfg::mark_point_as_public(&p_folded)?; + + Ok(()) + } +} + +impl ConstraintSynthesizer> for CycleFoldCircuit { + fn generate_constraints( + self, + cs: ConstraintSystemRef>, + ) -> Result<(), SynthesisError> { + self.fold_points(cs, Cfg::default()) + } +} diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 1a538b2b0..3a36f4993 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -1,122 +1,349 @@ use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; +use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, +}; +use ark_std::{marker::PhantomData, rand::RngCore, sync::Arc}; +use sonobe_fs::{ + FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemeFullGadget, + FoldingSchemePartialGadget, }; -use ark_std::{marker::PhantomData, rand::RngCore}; -use sonobe_fs::{FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemePartialGadget}; use sonobe_primitives::{ - circuits::{ConstraintSystemExt, FCircuit}, - commitments::{CommitmentDef}, + arithmetizations::{Arith, ArithConfig}, + circuits::{ArithExtractor, AssignmentsExtractor, ConstraintSystemExt, FCircuit}, + commitments::{CommitmentDef, CommitmentDefGadget}, relations::WitnessInstanceSampler, - traits::SonobeField, - transcripts::Transcript, + traits::{CF1, CF2, Dummy, Inputize, InputizeEmulated, SonobeCurve, SonobeField}, + transcripts::{ + Absorbable, Transcript, + griffin::{GriffinParams, sponge::GriffinSponge}, + }, }; -use crate::IVC; +use crate::{ + Error, IVC, + compilers::cyclefold::circuits::{AugmentedCircuit, CycleFoldCircuit, CycleFoldConfig}, +}; -mod circuits; +pub mod circuits; pub trait FoldingSchemeCycleFoldGadget: FoldingSchemePartialGadget { + type CFConfig: CycleFoldConfig< + C = <>::VC as CommitmentDef>::Commitment, + >; + type CFScalarVar; + const N_CYCLEFOLDS: usize; + + fn to_cyclefold_configs( + U: &>::RU, + u: &>::IU, + proof: &>::Proof, + rho: >::Challenge, + ) -> Vec; + fn to_cyclefold_inputs( U: Self::RU, u: Self::IU, UU: Self::RU, + proof: Self::Proof, rho: Self::Challenge, ) -> Result>, SynthesisError>; } -pub struct CycleFoldBasedIVC { - _fs1: PhantomData, - _fs2: PhantomData, +pub struct ProverKey, FS2: FoldingScheme<1, 1>>( + FS1::ProverKey, + FS1::DeciderKey, + FS2::ProverKey, + FS2::DeciderKey, + Arc>, + F, + ::Config, + ::Config, +); + +pub struct Proof, FS2: FoldingScheme<1, 1>>( + FS1::RW, + FS1::RU, + FS1::IW, + FS1::IU, + FS2::RW, + FS2::RU, +); + +impl, FS2: FoldingScheme<1, 1>> + Dummy<&ProverKey> for Proof +{ + fn dummy(pk: &ProverKey) -> Self { + let cfg1 = &pk.6; + let cfg2 = &pk.7; + + let W = FS1::RW::dummy(cfg1); + let U = FS1::RU::dummy(cfg1); + let w = FS1::IW::dummy(cfg1); + let u = FS1::IU::dummy(cfg1); + let cf_W = FS2::RW::dummy(cfg2); + let cf_U = FS2::RU::dummy(cfg2); + + Self(W, U, w, u, cf_W, cf_U) + } } -pub struct ProverKey { - poseidon_config: PoseidonConfig, - pp_hash: FC::Field, +pub struct CycleFoldBasedIVC { + _d: PhantomData<(C1, C2, FS1, FS2)>, } -impl IVC for CycleFoldBasedIVC +impl IVC for CycleFoldBasedIVC where - FS1: FoldingScheme< + C1: SonobeCurve, + FS1: FoldingSchemeCycleFoldGadget< + 1, + 1, + VC: CommitmentDefGadget< + ConstraintField = C1::ScalarField, + ScalarVar = FpVar, + >, + CFScalarVar = ::ScalarVar, + Native: FoldingScheme< + 1, + 1, + Arith: Arith + From>, + TranscriptField = C1::ScalarField, + VC: CommitmentDef, + >, + VerifierKey = (), + >, + FS2: FoldingSchemeFullGadget< 1, 1, - VC: CommitmentDef>::TranscriptField>, + Native: FoldingScheme< + 1, + 1, + Arith: Arith + From>, + TranscriptField = C1::ScalarField, + VC: CommitmentDef, + >, + VerifierKey = (), + VC: CommitmentDefGadget, >, - FS2: FoldingScheme<1, 1>, { - type Field = ::Scalar; - - type Config = (FS1::Config, FS2::Config); + type Field = C1::ScalarField; - type PublicParam = (FS1::PublicParam, FS2::PublicParam); + type Config = ( + >::Config, + >::Config, + Arc>, + ); - type ProverKey = ( - FS1::ProverKey, - FS1::DeciderKey, - FS2::ProverKey, - FS2::DeciderKey, - ProverKey, + type PublicParam = ( + >::PublicParam, + >::PublicParam, + Arc>, ); - type VerifierKey = (); + type ProverKey = ProverKey; + + type VerifierKey = ( + >::DeciderKey, + >::DeciderKey, + Arc>, + Self::Field, + ); - type Proof = (FS1::RW, FS1::RU, FS1::IW, FS1::IU, FS2::RW, FS2::RU); + type Proof = Proof; fn preprocess( - config: Self::Config, + (cfg1, cfg2, griffin_config): Self::Config, mut rng: impl RngCore, - ) -> Result { + ) -> Result { Ok(( - FS1::preprocess(config.0, &mut rng)?, - FS2::preprocess(config.1, &mut rng)?, + FS1::Native::preprocess(cfg1, &mut rng)?, + FS2::Native::preprocess(cfg2, &mut rng)?, + griffin_config, )) } fn generate_keys>( - pp: &Self::PublicParam, + (pp1, pp2, griffin_config): Self::PublicParam, step_circuit: &FC, - ) -> Result<(Self::ProverKey, Self::VerifierKey), crate::Error> { - todo!() + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error> { + let mut arith2 = >::Arith::default(); + + loop { + let cyclefold_circuit = CycleFoldCircuit::::default(); + + let cs = ArithExtractor::new(); + cs.execute_synthesizer(cyclefold_circuit)?; + let new_arith2 = cs.arith::<>::Arith>()?; + if new_arith2.config() == arith2.config() { + break; + } + arith2 = new_arith2; + } + + let mut arith1 = >::Arith::default(); + + loop { + let augmented_circuit = AugmentedCircuit:: { + griffin_config: griffin_config.clone(), + arith1_config: arith1.config(), + arith2_config: arith2.config(), + step_circuit, + }; + + let cs = ArithExtractor::new(); + cs.execute_synthesizer(augmented_circuit)?; + let new_arith1 = cs.arith::<>::Arith>()?; + if new_arith1.config() == arith1.config() { + break; + } + arith1 = new_arith1; + } + + let arith1_config = arith1.config().clone(); + let arith2_config = arith2.config().clone(); + + let (pk1, _, dk1) = FS1::Native::generate_keys(pp1, arith1)?; + let (pk2, _, dk2) = FS2::Native::generate_keys(pp2, arith2)?; + + let pp_hash = Zero::zero(); // TODO + + Ok(( + ProverKey( + pk1, + dk1.clone(), + pk2, + dk2.clone(), + griffin_config.clone(), + pp_hash, + arith1_config, + arith2_config, + ), + (dk1, dk2, griffin_config, pp_hash), + )) } fn prove>( - (pk_fs1, dk_fs1, pk_fs2, dk_fs2, pk): &Self::ProverKey, + ProverKey(pk1, dk1, pk2, dk2, griffin_config, pp_hash, arith1_config, arith2_config): &Self::ProverKey, step_circuit: &FC, i: usize, initial_state: &FC::State, current_state: &FC::State, external_inputs: FC::ExternalInputs, - (W, U, w, u, cfW, cfU): &Self::Proof, + Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, mut rng: impl RngCore, - ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), crate::Error> { - let poseidon = PoseidonSponge::new_with_pp_hash(&pk.poseidon_config, pk.pp_hash); - let sponge = poseidon.separate_domain("sponge".as_ref()); - let mut transcript = poseidon.separate_domain("transcript".as_ref()); + ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error> { + let hash = GriffinSponge::new_with_pp_hash(&griffin_config, *pp_hash); + let mut transcript = hash.separate_domain("transcript".as_ref()); - let (WW, UU, proof, challenge) = - FS1::prove(pk_fs1, &mut transcript, &[W], &[U], &[w], &[u], &mut rng)?; + let augmented_circuit = AugmentedCircuit:: { + griffin_config: griffin_config.clone(), + arith1_config, + arith2_config, + step_circuit, + }; - if i == 0 { - } else { + let mut WW = Dummy::dummy(arith1_config); + let mut UU = Dummy::dummy(arith1_config); + let mut proof = Dummy::dummy(arith1_config); + let mut cf_us = vec![Dummy::dummy(arith2_config); FS1::N_CYCLEFOLDS]; + let mut cf_proofs = vec![Dummy::dummy(arith2_config); FS1::N_CYCLEFOLDS]; + let mut cf_UU = Dummy::dummy(arith2_config); + let mut cf_WW = Dummy::dummy(arith2_config); + + if i != 0 { + cf_us.clear(); + cf_proofs.clear(); + + let challenge; + (WW, UU, proof, challenge) = + FS1::Native::prove(pk1, &mut transcript, &[W], &[U], &[w], &[u], &mut rng)?; + + let cf_configs = FS1::to_cyclefold_configs(&U, &u, &proof, challenge); + for cfg in cf_configs { + let cs = AssignmentsExtractor::new(); + cs.execute_fn(|cs| CycleFoldCircuit::default().fold_points(cs, cfg))?; + + let (cf_w, cf_u) = dk2.sample(cs.assignments()?, &mut rng)?; + + let cf_proof; + (cf_WW, cf_UU, cf_proof, _) = FS2::Native::prove( + pk2, + &mut transcript, + &[cf_W], + &[cf_U], + &[&cf_w], + &[&cf_u], + &mut rng, + )?; + cf_us.push(cf_u); + cf_proofs.push(cf_proof); + } } - let cs = ConstraintSystem::::new_ref(); + let cs = AssignmentsExtractor::new(); + let (next_state, external_outputs) = cs.execute_fn(|cs| { + augmented_circuit.compute_next_state( + cs, + *pp_hash, + i, + initial_state, + current_state, + external_inputs, + U, + u, + proof, + cf_U, + cf_us, + cf_proofs, + ) + })?; + + let (ww, uu) = dk1.sample(cs.assignments()?, &mut rng)?; - todo!() + Ok(( + next_state, + external_outputs, + Proof(WW, UU, ww, uu, cf_WW, cf_UU), + )) } fn verify>( - vk: &Self::VerifierKey, + (dk1, dk2, griffin_config, pp_hash): &Self::VerifierKey, i: usize, initial_state: &FC::State, current_state: &FC::State, - proof: &Self::Proof, - ) -> Result<(), crate::Error> { - todo!() + Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, + ) -> Result<(), Error> { + if i == 0 { + return (initial_state == current_state) + .then_some(()) + .ok_or(Error::IVCVerificationFail); + } + + let griffin = GriffinSponge::new_with_pp_hash(griffin_config, *pp_hash); + let mut sponge = griffin.separate_domain("sponge".as_ref()); + + let u_x = sponge + .add(&i) + .add(initial_state) + .add(current_state) + .add(U) + .add(cf_U) + .get_field_elements(2); + + if u.public_inputs() != &u_x[..] { + return Err(Error::IVCVerificationFail); + } + + FS1::Native::decide_running(&dk1, &W, &U)?; + FS1::Native::decide_incoming(&dk1, &w, &u)?; + FS2::Native::decide_running(&dk2, &cf_W, &cf_U)?; + + Ok(()) } } diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index 2f04c7d07..183910db0 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -1,16 +1,21 @@ use ark_ff::PrimeField; +use ark_relations::gr1cs::SynthesisError; use ark_std::rand::RngCore; -use sonobe_primitives::circuits::FCircuit; +use sonobe_primitives::{circuits::FCircuit, traits::Dummy}; use thiserror::Error; pub mod compilers; #[derive(Debug, Error)] pub enum Error { - #[error("Arithmetization error: {0}")] + #[error(transparent)] ArithError(#[from] sonobe_primitives::arithmetizations::Error), - #[error("Folding error: {0}")] + #[error(transparent)] FoldingError(#[from] sonobe_fs::Error), + #[error(transparent)] + SynthesisError(#[from] SynthesisError), + #[error("IVC verification failed")] + IVCVerificationFail, } pub trait IVC { @@ -18,19 +23,19 @@ pub trait IVC { type Config; type PublicParam; - type ProverKey; - type VerifierKey; + type ProverKey; + type VerifierKey; type Proof; fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; fn generate_keys>( - pp: &Self::PublicParam, + pp: Self::PublicParam, step_circuit: &FC, - ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; fn prove>( - pk: &Self::ProverKey, + pk: &Self::ProverKey, step_circuit: &FC, i: usize, initial_state: &FC::State, @@ -41,7 +46,7 @@ pub trait IVC { ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error>; fn verify>( - vk: &Self::VerifierKey, + vk: &Self::VerifierKey, i: usize, initial_state: &FC::State, current_state: &FC::State, @@ -50,7 +55,7 @@ pub trait IVC { } pub struct IVCStatefulProver { - pub pk: I::ProverKey, + pub pk: I::ProverKey, pub step_circuit: FC, pub i: usize, pub initial_state: FC::State, @@ -60,18 +65,22 @@ pub struct IVCStatefulProver { impl, I: IVC> IVCStatefulProver { pub fn new( - pk: I::ProverKey, + pk: I::ProverKey, step_circuit: FC, initial_state: FC::State, - initial_proof: I::Proof, - ) -> Result { + ) -> Result + where + I::Proof: for<'a> Dummy<&'a I::ProverKey>, + { + let current_proof = I::Proof::dummy(&pk); + Ok(Self { pk, step_circuit, i: 0, current_state: initial_state.clone(), initial_state, - current_proof: initial_proof, + current_proof, }) } From ec8e739a51efe17fb4649fbaf14e13c8f3801e85 Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 17 Nov 2025 06:09:37 +0800 Subject: [PATCH 42/93] Convenience trait for group based folding schemes --- crates/fs/src/lib.rs | 45 ++- crates/ivc/Cargo.toml | 2 + .../ivc/src/compilers/cyclefold/circuits.rs | 276 +++++------------- crates/ivc/src/compilers/cyclefold/mod.rs | 131 ++++----- crates/ivc/src/compilers/mod.rs | 1 + crates/ivc/src/lib.rs | 64 +++- 6 files changed, 230 insertions(+), 289 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index c8b728d8e..68248f408 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -11,12 +11,13 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{borrow::Borrow, fmt::Debug, rand::RngCore}; use sonobe_primitives::{ + algebra::group::emulated::EmulatedAffineVar, arithmetizations::{Arith, ArithConfig}, circuits::AssignmentsOwned, - commitments::{CommitmentDef, CommitmentDefGadget}, + commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, sumcheck::Error as SumCheckError, - traits::{Dummy, SonobeField}, + traits::{CF2, Dummy, SonobeField}, transcripts::{Absorbable, AbsorbableVar, Transcript, TranscriptGadget}, }; use thiserror::Error; @@ -391,6 +392,38 @@ impl FoldingInstanceVar for PlainInstanceVar { } } +pub trait GroupBasedFoldingSchemePrimary: + FoldingScheme< + M, + N, + VC: GroupBasedCommitment, + TranscriptField = <>::VC as CommitmentDef>::Scalar, +> +{ + type Gadget: FoldingSchemePartialGadget< + M, + N, + Native = Self, + VC = ::Gadget2, + >; +} + +pub trait GroupBasedFoldingSchemeSecondary: + FoldingScheme< + M, + N, + VC: GroupBasedCommitment, + TranscriptField = CF2<<>::VC as CommitmentDef>::Commitment>, +> +{ + type Gadget: FoldingSchemeFullGadget< + M, + N, + Native = Self, + VC = ::Gadget1, + >; +} + pub trait FoldingSchemePartialGadget { type Native: FoldingScheme; @@ -416,8 +449,8 @@ pub trait FoldingSchemePartialGadget { fn verify_hinted( vk: &Self::VerifierKey, transcript: &mut impl TranscriptGadget<::ConstraintField>, - Us: &[impl Borrow; M], - us: &[impl Borrow; N], + Us: [&Self::RU; M], + us: [&Self::IU; N], proof: &Self::Proof, ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; } @@ -429,8 +462,8 @@ pub trait FoldingSchemeFullGadget: fn verify( vk: &Self::VerifierKey, transcript: &mut impl TranscriptGadget<::ConstraintField>, - Us: &[impl Borrow; M], - us: &[impl Borrow; N], + Us: [&Self::RU; M], + us: [&Self::IU; N], proof: &Self::Proof, ) -> Result; } diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml index be76625ee..5924a988b 100644 --- a/crates/ivc/Cargo.toml +++ b/crates/ivc/Cargo.toml @@ -7,10 +7,12 @@ repository.workspace = true [dependencies] ark-crypto-primitives = { workspace = true, features = ["constraints", "sponge", "crh"] } +ark-ec = { workspace = true } ark-ff = { workspace = true, features = ["asm"] } ark-r1cs-std = { workspace = true } ark-relations = { workspace = true } ark-std = { workspace = true, features = ["getrandom"] } +num-bigint = { workspace = true, features = ["rand"] } thiserror = { workspace = true } sonobe-primitives = { workspace = true } diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index 017036671..3cc9d088a 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -14,7 +14,7 @@ use ark_relations::gr1cs::{ use ark_std::{marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_fs::{ FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemeFullGadget, - FoldingSchemePartialGadget, + FoldingSchemePartialGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ algebra::Val, @@ -33,31 +33,35 @@ use crate::compilers::cyclefold::FoldingSchemeCycleFoldGadget; pub struct AugmentedCircuit< 'a, - FS1: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, - FS2: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + FS1: GroupBasedFoldingSchemePrimary<1, 1>, + FS2: GroupBasedFoldingSchemeSecondary<1, 1>, FC: FCircuit, > { pub griffin_config: Arc>, - pub arith1_config: &'a <>::Arith as Arith>::Config, - pub arith2_config: &'a <>::Arith as Arith>::Config, + pub arith1_config: &'a ::Config, + pub arith2_config: &'a ::Config, pub step_circuit: &'a FC, } -impl<'a, FS1, FS2, FC: FCircuit> AugmentedCircuit<'a, FS1, FS2, FC> +impl<'a, FS1, FS2, FC> AugmentedCircuit<'a, FS1, FS2, FC> where FS1: FoldingSchemeCycleFoldGadget< - 1, - 1, - VerifierKey = (), - VC: CommitmentDefGadget>, - CFScalarVar = ::ScalarVar, + 1, + 1, + Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, - FS2: FoldingSchemeFullGadget< - 1, - 1, - VerifierKey = (), - VC: CommitmentDefGadget, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, + >, + FC: FCircuit::Scalar>, { pub fn compute_next_state( &self, @@ -67,12 +71,12 @@ where initial_state: &FC::State, current_state: &FC::State, external_inputs: FC::ExternalInputs, - U: &>::Value, - u: &>::Value, - proof: >::Value, - cf_U: &>::Value, - cf_us: Vec<>::Value>, - cf_proofs: Vec<>::Value>, + U: &FS1::RU, + u: &FS1::IU, + proof: FS1::Proof, + cf_U: &FS2::RU, + cf_us: Vec, + cf_proofs: Vec, ) -> Result<(FC::State, FC::ExternalOutputs), SynthesisError> { let hash = GriffinSpongeVar::new_with_pp_hash( &self.griffin_config, @@ -89,22 +93,14 @@ where let initial_state = FC::StateVar::new_witness(cs.clone(), || Ok(initial_state))?; let current_state = FC::StateVar::new_witness(cs.clone(), || Ok(current_state))?; - let U_dummy = FS1::RU::new_constant( - cs.clone(), - >::Value::dummy(self.arith1_config), - )?; - let U = FS1::RU::new_witness(cs.clone(), || Ok(U))?; - let proof = FS1::Proof::new_witness(cs.clone(), || Ok(proof))?; + let U_dummy = AllocVar::new_constant(cs.clone(), FS1::RU::dummy(self.arith1_config))?; + let U = AllocVar::new_witness(cs.clone(), || Ok(U))?; + let proof = AllocVar::new_witness(cs.clone(), || Ok(proof))?; - let cf_U_dummy = FS2::RU::new_constant( - cs.clone(), - >::Value::dummy(self.arith2_config), - )?; - let cf_U = FS2::RU::new_witness(cs.clone(), || Ok(cf_U))?; + let cf_U_dummy = AllocVar::new_constant(cs.clone(), FS2::RU::dummy(self.arith2_config))?; + let cf_U = AllocVar::new_witness(cs.clone(), || Ok(cf_U))?; let cf_proofs = Vec::new_witness(cs.clone(), || Ok(cf_proofs))?; - println!("{}", cs.num_constraints()); - let u_x = sponge .clone() .add(&i)? @@ -113,30 +109,27 @@ where .add(&U)? .add(&cf_U)? .get_field_elements(2)?; - let u = FS1::IU::new_witness_with_public_inputs(cs.clone(), u, u_x)?; - let (UU, rho) = FS1::verify_hinted(&(), &mut transcript, &[&U], &[&u], &proof)?; + let u = FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), u, u_x)?; + let (UU, rho) = FS1::Gadget::verify_hinted(&(), &mut transcript, [&U], [&u], &proof)?; let actual_UU = is_basecase.select(&U_dummy, &UU)?; - println!("{}", cs.num_constraints()); - let mut cf_UU = cf_U; for ((cf_u, cf_u_x), cf_proof) in cf_us .iter() - .zip(FS1::to_cyclefold_inputs(U, u, UU, proof, rho)?) + .zip(FS1::to_cyclefold_inputs([U], [u], UU, proof, rho)?) .zip(&cf_proofs) { - let cf_u = FS2::IU::new_witness_with_public_inputs(cs.clone(), cf_u, cf_u_x)?; - cf_UU = FS2::verify(&(), &mut transcript, &[cf_UU], &[cf_u], cf_proof)?; + let cf_u = + FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), cf_u, cf_u_x)?; + cf_UU = FS2::Gadget::verify(&(), &mut transcript, [&cf_UU], [&cf_u], cf_proof)?; } let actual_cf_UU = is_basecase.select(&cf_U_dummy, &cf_UU)?; - println!("{}", cs.num_constraints()); - - let (next_state, external_outputs) = - self.step_circuit - .generate_step_constraints(i, current_state, external_inputs)?; - - println!("{}", cs.num_constraints()); + let (next_state, external_outputs) = self.step_circuit.generate_step_constraints( + i, + current_state, + external_inputs, + )?; let uu_x = sponge .clone() @@ -159,8 +152,6 @@ where Ok(uu_x.value().unwrap_or(vec![Default::default(); uu_x.len()])) })?)?; - println!("{}", cs.num_constraints()); - if cs.is_in_setup_mode() { Ok((self.step_circuit.dummy_state(), external_outputs)) } else { @@ -169,22 +160,23 @@ where } } -impl<'a, FS1, FS2, FC: FCircuit> ConstraintSynthesizer - for AugmentedCircuit<'a, FS1, FS2, FC> +impl<'a, FS1, FS2, FC> ConstraintSynthesizer for AugmentedCircuit<'a, FS1, FS2, FC> where FS1: FoldingSchemeCycleFoldGadget< - 1, - 1, - VerifierKey = (), - VC: CommitmentDefGadget>, - CFScalarVar = ::ScalarVar, - >, - FS2: FoldingSchemeFullGadget< - 1, - 1, - VerifierKey = (), - VC: CommitmentDefGadget, + 1, + 1, + Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, + >, + FS2: GroupBasedFoldingSchemeSecondary< + 1, + 1, + Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef>, + >, + FC: FCircuit::Scalar>, { fn generate_constraints( self, @@ -201,8 +193,8 @@ where &Dummy::dummy(self.arith1_config), Dummy::dummy(self.arith1_config), &Dummy::dummy(self.arith2_config), - vec![Dummy::dummy(self.arith2_config); >::Value::N_COMMITMENTS], - vec![Dummy::dummy(self.arith2_config); >::Value::N_COMMITMENTS], + vec![Dummy::dummy(self.arith2_config); FS1::N_CYCLEFOLDS], + vec![Dummy::dummy(self.arith2_config); FS1::N_CYCLEFOLDS], ) .map(|_| ()) } @@ -220,22 +212,14 @@ pub trait CycleFoldConfig: Sized + Default { /// `N_INPUT_POINTS` specifies the number of input points that are folded in /// [`CycleFoldCircuit`] via random linear combinations. const N_INPUT_POINTS: usize; - /// `N_UNIQUE_RANDOMNESSES` specifies the number of *unique* randomnesses - /// allocated in [`CycleFoldCircuit`]. Although the linear combination in - /// general consists of multiple randomnesses, some folding schemes (such as - /// Nova and HyperNova) only need a single one. Thus, by setting this value, - /// the circuit can learn how many randomnesses are used and how long the - /// public inputs vector should be. - const N_UNIQUE_RANDOMNESSES: usize; - /// `RANDOMNESS_BIT_LENGTH` is the maximum bit length of a randomness `r_i`. - const RANDOMNESS_BIT_LENGTH: usize; + const N_INPUT_RANDOMNESS_BITS: usize; /// `FIELD_CAPACITY` is the maximum number of bits that can be stored in a /// field element. /// /// By default, `FIELD_CAPACITY` is set to `MODULUS_BIT_SIZE - 1`. /// - /// Given a randomness `r_i` with `RANDOMNESS_BIT_LENGTH` bits, we need - /// `RANDOMNESS_BIT_LENGTH / FIELD_CAPACITY` field elements to represent it + /// Given a randomness with `N_INPUT_RANDOMNESS_BITS` bits, we need + /// `N_INPUT_RANDOMNESS_BITS / FIELD_CAPACITY` field elements to pack it /// *compactly* in-circuit. const FIELD_CAPACITY: usize = CF2::::MODULUS_BIT_SIZE as usize - 1; @@ -246,66 +230,25 @@ pub trait CycleFoldConfig: Sized + Default { /// * In ProtoGalaxy, this is `|[..., r_i, ...]| + |p_i| * n_points + |P|`. /// /// As explained above, `|r|` (i.e., the length of a single randomness) is - /// `RANDOMNESS_BIT_LENGTH / FIELD_CAPACITY`. - /// When there are multiple randomnesses, the length of `|[..., r_i, ...]|` - /// is `RANDOMNESS_BIT_LENGTH * N_UNIQUE_RANDOMNESSES / FIELD_CAPACITY`, as - /// the bits of all randomnesses are concatenated before being packed into - /// field elements. + /// `N_INPUT_RANDOMNESS_BITS / FIELD_CAPACITY`. /// The length of a point `p_i` when treated as public inputs is 2, as we /// only need the `x` and `y` coordinates of the point. /// - /// Thus, `IO_LEN` is `RANDOMNESS_BIT_LENGTH * N_UNIQUE_RANDOMNESSES / FIELD_CAPACITY + 2 * (N_INPUT_POINTS + 1)`. + /// Thus, `IO_LEN` is: + /// `N_INPUT_RANDOMNESS_BITS / FIELD_CAPACITY + 2 * (N_INPUT_POINTS + 1)`. const IO_LEN: usize = { - (Self::RANDOMNESS_BIT_LENGTH * Self::N_UNIQUE_RANDOMNESSES).div_ceil(Self::FIELD_CAPACITY) + Self::N_INPUT_RANDOMNESS_BITS.div_ceil(Self::FIELD_CAPACITY) + 2 * (Self::N_INPUT_POINTS + 1) }; - /// `alloc_points` allocates the points that are going to be folded in the - /// [`CycleFoldCircuit`] via random linear combinations. - /// - /// The implementation must allocate the points as *witness* variables (i.e. - /// by calling [`AllocVar::new_witness`]) first, then mark them as public - /// inputs by calling [`CycleFoldConfig::mark_point_as_public`], and finally - /// return the allocated witness variables. - /// - /// While it is possible to allocate the points as public inputs directly, - /// we do not use this approach because this will create a longer vector of - /// public inputs, which is not ideal for the augmented step circuit on the - /// primary curve. - fn alloc_points( - &self, - cs: ConstraintSystemRef>, - ) -> Result::Var>, SynthesisError>; - - /// `alloc_randomnesses` allocates the randomnesses used as coefficients of - /// the random linear combinations in the `CycleFoldCircuit`. - /// - /// The implementation must allocate the randomnesses as *witness* variables - /// (i.e. by calling [`AllocVar::new_witness`]) first, then mark them as - /// public inputs by calling [`CycleFoldConfig::mark_point_as_public`], and - /// finally return the allocated witness variables. - /// - /// See [`CycleFoldConfig::alloc_points`] for the reason why they need to be - /// allocated as witness variables first and converted to public later. - /// - /// In addition, because the circuit computes `P = (P + p_i) * r_i` for each - /// `i` from `N_INPUT_POINTS - 1` down to `0`, the actual linear combination - /// is `P = r_0 * p_0 + (r_0 r_1) * p_1 + (r_0 r_1 r_2) * p_2 + ...`. Thus, - /// to compute `P = R_0 p_0 + R_1 p_1 + R_2 p_2 + ...`, the implementation - /// should return `r_0 = R_0, r_1 = R_1 / R_0, ..., r_i = R_i / R_{i - 1}`. - /// A special case is `R_i = R^i`, where the allocated randomnesses become - /// `r_0 = 1, r_1 = r_2 = ... = R`. - fn alloc_randomnesses( - &self, - cs: ConstraintSystemRef>, - ) -> Result>>>, SynthesisError>; - /// `mark_point_as_public` marks a point as public. /// /// The final vector of public inputs is shorter than the result of calling /// [`AllocVar::new_input`], because we only need the x and y coordinates of /// the point, but the `infinity` flag is not necessary. - fn mark_point_as_public(point: &::Var) -> Result<(), SynthesisError> { + fn mark_point_as_public( + point: &impl CurveVar>, + ) -> Result<(), SynthesisError> { for x in &point.to_constraint_field()?[..2] { // This line "converts" `x` from a witness to a public input. // Instead of directly modifying the constraint system, we explicitly @@ -316,23 +259,13 @@ pub trait CycleFoldConfig: Sized + Default { // computing them outside the circuit. // - `.enforce_equal()` prevents a malicious prover from claiming wrong // public inputs that are not the honest `x` computed in-circuit. - FpVar::new_input(x.cs().clone(), || x.value())?.enforce_equal(x)?; + FpVar::new_input(x.cs(), || x.value())?.enforce_equal(x)?; } Ok(()) } - /// `mark_randomness_as_public` marks randomness as public. - /// - /// The final vector of public inputs is shorter than the result of calling - /// [`AllocVar::new_input`], because we pack the bits of randomness into - /// a compact field elements. - fn mark_randomness_as_public(r: &[Boolean>]) -> Result<(), SynthesisError> { - for bits in r.chunks(Self::FIELD_CAPACITY) { - let x = Boolean::le_bits_to_fp(bits)?; - FpVar::new_input(x.cs().clone(), || x.value())?.enforce_equal(&x)?; - } - Ok(()) - } + fn verify_point_rlc(&self, cs: ConstraintSystemRef>) + -> Result<(), SynthesisError>; } #[derive(Debug, Clone, Default)] @@ -340,76 +273,11 @@ pub struct CycleFoldCircuit { _cfg: PhantomData, } -impl CycleFoldCircuit { - pub fn fold_points( - &self, - cs: ConstraintSystemRef>, - cfg: Cfg, - ) -> Result<(), SynthesisError> { - let rs = cfg.alloc_randomnesses(cs.clone())?; - let points = cfg.alloc_points(cs.clone())?; - - #[cfg(test)] - { - assert_eq!(Cfg::N_INPUT_POINTS, points.len()); - assert_eq!(Cfg::N_INPUT_POINTS, rs.len()); - for r in &rs { - assert_eq!(Cfg::RANDOMNESS_BIT_LENGTH, r.len()); - } - } - - // A slightly optimized version of `scalar_mul_le`. - fn point_mul( - point: &C::Var, - r: &[Boolean>], - ) -> Result { - if r.is_constant() { - let r = CF1::::from( as PrimeField>::BigInt::from_bits_le(&r.value()?)); - if r.is_one() { - return Ok(point.clone()); - } - } - point.scalar_mul_le(r.iter()) - } - - // Given a vector of points (over the primary curve) that are obtained - // from the instances of the folding scheme, we fold them *natively* in - // the CycleFold circuit (over the secondary curve). - // * In Nova, we need to compute P = p_0 + R * p_1. - // - for the cmW we're computing: U_i1.cmW = U_i.cmW + R * u_i.cmW - // - for the cmE we're computing: U_i1.cmE = U_i.cmE + R * cmT + R^2 * u_i.cmE, where u_i.cmE - // is assumed to be 0, so, U_i1.cmE = U_i.cmE + R * cmT - // * In HyperNova, we need to compute P = p_0 + R * p_1 + R^2 * p_2 + ... + R^{n-1} * p_{n-1}. - // * In ProtoGalaxy, we need to compute P = R_0 * p_0 + R_1 * p_1 + R_2 * p_2 + ... + R_{n-1} * p_{n-1}. - // - // To handle HyperNova more efficiently (with less constraints), we do - // P = ((((p_{n-1} * R) + p_{n-2}) * R + p_{n-3}) * R + ...) * R + p_0. - // This can be done iteratively by computing P = (P + p_i) * R. - // - // We further generalize this to support ProtoGalaxy, which now becomes - // P = (((((p_{n-1} * r_{n-1}) + p_{n-2}) * r_{n-2} + p_{n-3}) * r_{n-3} + ...) * r_1 + p_0) * r_0 - // - // Here, r_0 = 1, r_1 = r_2 = ... = r_{n-1} = R for Nova and HyperNova, - // and r_i = R_i / R_{i - 1} for ProtoGalaxy. - let mut p_folded = point_mul::( - &points[Cfg::N_INPUT_POINTS - 1], - &rs[Cfg::N_INPUT_POINTS - 1], - )?; - for i in (0..Cfg::N_INPUT_POINTS - 1).rev() { - p_folded = point_mul::(&(p_folded + &points[i]), &rs[i])?; - } - - Cfg::mark_point_as_public(&p_folded)?; - - Ok(()) - } -} - impl ConstraintSynthesizer> for CycleFoldCircuit { fn generate_constraints( self, cs: ConstraintSystemRef>, ) -> Result<(), SynthesisError> { - self.fold_points(cs, Cfg::default()) + Cfg::default().verify_point_rlc(cs) } } diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 3a36f4993..36cdf38ad 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -1,15 +1,17 @@ use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; +use ark_ec::{CurveGroup, PrimeGroup}; use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; use ark_relations::gr1cs::{ ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, }; -use ark_std::{marker::PhantomData, rand::RngCore, sync::Arc}; +use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_fs::{ FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemeFullGadget, - FoldingSchemePartialGadget, + FoldingSchemePartialGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ + algebra::field::emulated::EmulatedFieldVar, arithmetizations::{Arith, ArithConfig}, circuits::{ArithExtractor, AssignmentsExtractor, ConstraintSystemExt, FCircuit}, commitments::{CommitmentDef, CommitmentDefGadget}, @@ -29,30 +31,36 @@ use crate::{ pub mod circuits; pub trait FoldingSchemeCycleFoldGadget: - FoldingSchemePartialGadget + GroupBasedFoldingSchemePrimary { - type CFConfig: CycleFoldConfig< - C = <>::VC as CommitmentDef>::Commitment, - >; - - type CFScalarVar; + type CFConfig: CycleFoldConfig::Commitment>; const N_CYCLEFOLDS: usize; fn to_cyclefold_configs( - U: &>::RU, - u: &>::IU, - proof: &>::Proof, - rho: >::Challenge, + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + proof: &Self::Proof, + rho: Self::Challenge, ) -> Vec; fn to_cyclefold_inputs( - U: Self::RU, - u: Self::IU, - UU: Self::RU, - proof: Self::Proof, - rho: Self::Challenge, - ) -> Result>, SynthesisError>; + Us: [>::RU; M], + us: [>::IU; N], + UU: >::RU, + proof: >::Proof, + rho: >::Challenge, + ) -> Result< + Vec< + Vec< + EmulatedFieldVar< + ::Scalar, + CF2<::Commitment>, + >, + >, + >, + SynthesisError, + >; } pub struct ProverKey, FS2: FoldingScheme<1, 1>>( @@ -93,76 +101,59 @@ impl, FS2: FoldingScheme<1, 1>> } } -pub struct CycleFoldBasedIVC { - _d: PhantomData<(C1, C2, FS1, FS2)>, +pub struct CycleFoldBasedIVC { + _d: PhantomData<(FS1, FS2)>, } -impl IVC for CycleFoldBasedIVC +impl IVC for CycleFoldBasedIVC where - C1: SonobeCurve, FS1: FoldingSchemeCycleFoldGadget< 1, 1, - VC: CommitmentDefGadget< - ConstraintField = C1::ScalarField, - ScalarVar = FpVar, - >, - CFScalarVar = ::ScalarVar, - Native: FoldingScheme< - 1, - 1, - Arith: Arith + From>, - TranscriptField = C1::ScalarField, - VC: CommitmentDef, + Arith: From::Commitment>>>, + Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, - VerifierKey = (), >, - FS2: FoldingSchemeFullGadget< + FS2: GroupBasedFoldingSchemeSecondary< 1, 1, - Native: FoldingScheme< - 1, - 1, - Arith: Arith + From>, - TranscriptField = C1::ScalarField, - VC: CommitmentDef, + Arith: From::Commitment>>>, + Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, - VerifierKey = (), - VC: CommitmentDefGadget, >, { - type Field = C1::ScalarField; + type Field = ::Scalar; - type Config = ( - >::Config, - >::Config, - Arc>, - ); + type Config = (FS1::Config, FS2::Config, Arc>); type PublicParam = ( - >::PublicParam, - >::PublicParam, + FS1::PublicParam, + FS2::PublicParam, Arc>, ); - type ProverKey = ProverKey; + type ProverKey = ProverKey; type VerifierKey = ( - >::DeciderKey, - >::DeciderKey, + FS1::DeciderKey, + FS2::DeciderKey, Arc>, Self::Field, ); - type Proof = Proof; + type Proof = Proof; fn preprocess( (cfg1, cfg2, griffin_config): Self::Config, mut rng: impl RngCore, ) -> Result { Ok(( - FS1::Native::preprocess(cfg1, &mut rng)?, - FS2::Native::preprocess(cfg2, &mut rng)?, + FS1::preprocess(cfg1, &mut rng)?, + FS2::preprocess(cfg2, &mut rng)?, griffin_config, )) } @@ -171,21 +162,21 @@ where (pp1, pp2, griffin_config): Self::PublicParam, step_circuit: &FC, ) -> Result<(Self::ProverKey, Self::VerifierKey), Error> { - let mut arith2 = >::Arith::default(); + let mut arith2 = FS2::Arith::default(); loop { let cyclefold_circuit = CycleFoldCircuit::::default(); let cs = ArithExtractor::new(); cs.execute_synthesizer(cyclefold_circuit)?; - let new_arith2 = cs.arith::<>::Arith>()?; + let new_arith2 = cs.arith::()?; if new_arith2.config() == arith2.config() { break; } arith2 = new_arith2; } - let mut arith1 = >::Arith::default(); + let mut arith1 = FS1::Arith::default(); loop { let augmented_circuit = AugmentedCircuit:: { @@ -197,7 +188,7 @@ where let cs = ArithExtractor::new(); cs.execute_synthesizer(augmented_circuit)?; - let new_arith1 = cs.arith::<>::Arith>()?; + let new_arith1 = cs.arith::()?; if new_arith1.config() == arith1.config() { break; } @@ -207,8 +198,8 @@ where let arith1_config = arith1.config().clone(); let arith2_config = arith2.config().clone(); - let (pk1, _, dk1) = FS1::Native::generate_keys(pp1, arith1)?; - let (pk2, _, dk2) = FS2::Native::generate_keys(pp2, arith2)?; + let (pk1, _, dk1) = FS1::generate_keys(pp1, arith1)?; + let (pk2, _, dk2) = FS2::generate_keys(pp2, arith2)?; let pp_hash = Zero::zero(); // TODO @@ -261,17 +252,17 @@ where let challenge; (WW, UU, proof, challenge) = - FS1::Native::prove(pk1, &mut transcript, &[W], &[U], &[w], &[u], &mut rng)?; + FS1::prove(pk1, &mut transcript, &[W], &[U], &[w], &[u], &mut rng)?; - let cf_configs = FS1::to_cyclefold_configs(&U, &u, &proof, challenge); + let cf_configs = FS1::to_cyclefold_configs(&[U], &[u], &proof, challenge); for cfg in cf_configs { let cs = AssignmentsExtractor::new(); - cs.execute_fn(|cs| CycleFoldCircuit::default().fold_points(cs, cfg))?; + cs.execute_fn(|cs| cfg.verify_point_rlc(cs))?; let (cf_w, cf_u) = dk2.sample(cs.assignments()?, &mut rng)?; let cf_proof; - (cf_WW, cf_UU, cf_proof, _) = FS2::Native::prove( + (cf_WW, cf_UU, cf_proof, _) = FS2::prove( pk2, &mut transcript, &[cf_W], @@ -340,9 +331,9 @@ where return Err(Error::IVCVerificationFail); } - FS1::Native::decide_running(&dk1, &W, &U)?; - FS1::Native::decide_incoming(&dk1, &w, &u)?; - FS2::Native::decide_running(&dk2, &cf_W, &cf_U)?; + FS1::decide_running(&dk1, &W, &U)?; + FS1::decide_incoming(&dk1, &w, &u)?; + FS2::decide_running(&dk2, &cf_W, &cf_U)?; Ok(()) } diff --git a/crates/ivc/src/compilers/mod.rs b/crates/ivc/src/compilers/mod.rs index 41e86f249..764a51d10 100644 --- a/crates/ivc/src/compilers/mod.rs +++ b/crates/ivc/src/compilers/mod.rs @@ -1 +1,2 @@ pub mod cyclefold; + diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index 183910db0..88bd3b592 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -25,7 +25,7 @@ pub trait IVC { type PublicParam; type ProverKey; type VerifierKey; - type Proof; + type Proof: for<'a> Dummy<&'a Self::ProverKey>; fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; @@ -68,19 +68,14 @@ impl, I: IVC> IVCStatefulProver { pk: I::ProverKey, step_circuit: FC, initial_state: FC::State, - ) -> Result - where - I::Proof: for<'a> Dummy<&'a I::ProverKey>, - { - let current_proof = I::Proof::dummy(&pk); - + ) -> Result { Ok(Self { - pk, step_circuit, i: 0, current_state: initial_state.clone(), initial_state, - current_proof, + current_proof: I::Proof::dummy(&pk), + pk, }) } @@ -105,3 +100,54 @@ impl, I: IVC> IVCStatefulProver { Ok(external_outputs) } } + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective as C1}; + use ark_crypto_primitives::sponge::{CryptographicSponge, poseidon::PoseidonSponge}; + use ark_ff::UniformRand; + use ark_grumpkin::Projective as C2; + use ark_std::{error::Error, rand::Rng, sync::Arc, test_rng}; + use sonobe_fs::{ + FoldingScheme, FoldingSchemeFullGadget, FoldingSchemePartialGadget, PlainInstance as IU, + PlainInstanceVar as IUVar, PlainWitness as IW, PlainWitnessVar as IWVar, + }; + use sonobe_primitives::{ + arithmetizations::Arith, + circuits::utils::CircuitForTest, + commitments::pedersen::{Pedersen, PedersenEmulatedGadget, PedersenGadget}, + traits::Dummy, + transcripts::{Transcript, griffin::GriffinParams, poseidon::poseidon_canonical_config}, + }; + + use super::*; + + pub fn test_ivc>( + config: I::Config, + step_circuit: F, + external_inputs_vec: Vec, + mut rng: impl Rng, + ) -> Result<(), Box> { + let pp = I::preprocess(config, &mut rng)?; + + let (pk, vk) = I::generate_keys(pp, &step_circuit)?; + + let initial_state = step_circuit.dummy_state(); + + let mut prover = IVCStatefulProver::<_, I>::new(pk, step_circuit, initial_state)?; + + for external_inputs in external_inputs_vec { + prover.prove_step(external_inputs, &mut rng)?; + + I::verify::( + &vk, + prover.i, + &prover.initial_state, + &prover.current_state, + &prover.current_proof, + )?; + } + + Ok(()) + } +} From a57c9b6a015b8d3d7ef90de09a03dc14e7efd4dc Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 17 Nov 2025 07:48:25 +0800 Subject: [PATCH 43/93] Correctly update CF running instance & witness --- crates/fs/src/lib.rs | 1 - .../ivc/src/compilers/cyclefold/circuits.rs | 73 +++++++++---------- crates/ivc/src/compilers/cyclefold/mod.rs | 21 ++---- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 68248f408..e1bc7e0fb 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -11,7 +11,6 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{borrow::Borrow, fmt::Debug, rand::RngCore}; use sonobe_primitives::{ - algebra::group::emulated::EmulatedAffineVar, arithmetizations::{Arith, ArithConfig}, circuits::AssignmentsOwned, commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index 3cc9d088a..291f3c532 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -3,18 +3,15 @@ use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, boolean::Boolean, - convert::ToConstraintFieldGadget, eq::EqGadget, fields::{FieldVar, fp::FpVar}, groups::CurveVar, }; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, -}; -use ark_std::{marker::PhantomData, rand::RngCore, sync::Arc}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_std::{marker::PhantomData, sync::Arc}; use sonobe_fs::{ - FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemeFullGadget, - FoldingSchemePartialGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, + FoldingInstanceVar, FoldingSchemeFullGadget, FoldingSchemePartialGadget, + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ algebra::Val, @@ -29,7 +26,7 @@ use sonobe_primitives::{ }, }; -use crate::compilers::cyclefold::FoldingSchemeCycleFoldGadget; +use crate::compilers::cyclefold::FoldingSchemeCycleFoldExt; pub struct AugmentedCircuit< 'a, @@ -45,22 +42,22 @@ pub struct AugmentedCircuit< impl<'a, FS1, FS2, FC> AugmentedCircuit<'a, FS1, FS2, FC> where - FS1: FoldingSchemeCycleFoldGadget< - 1, - 1, - Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, FS2: GroupBasedFoldingSchemeSecondary< - 1, - 1, - Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + 1, + 1, + Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, FC: FCircuit::Scalar>, { pub fn compute_next_state( @@ -125,11 +122,9 @@ where } let actual_cf_UU = is_basecase.select(&cf_U_dummy, &cf_UU)?; - let (next_state, external_outputs) = self.step_circuit.generate_step_constraints( - i, - current_state, - external_inputs, - )?; + let (next_state, external_outputs) = + self.step_circuit + .generate_step_constraints(i, current_state, external_inputs)?; let uu_x = sponge .clone() @@ -162,20 +157,20 @@ where impl<'a, FS1, FS2, FC> ConstraintSynthesizer for AugmentedCircuit<'a, FS1, FS2, FC> where - FS1: FoldingSchemeCycleFoldGadget< - 1, - 1, - Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + FS1: FoldingSchemeCycleFoldExt< + 1, + 1, + Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, FS2: GroupBasedFoldingSchemeSecondary< - 1, - 1, - Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef>, - >, + 1, + 1, + Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef>, + >, FC: FCircuit::Scalar>, { fn generate_constraints( @@ -265,7 +260,7 @@ pub trait CycleFoldConfig: Sized + Default { } fn verify_point_rlc(&self, cs: ConstraintSystemRef>) - -> Result<(), SynthesisError>; + -> Result<(), SynthesisError>; } #[derive(Debug, Clone, Default)] diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 36cdf38ad..5a91923d4 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -1,14 +1,9 @@ -use ark_crypto_primitives::sponge::poseidon::{PoseidonConfig, PoseidonSponge}; -use ark_ec::{CurveGroup, PrimeGroup}; use ark_ff::{PrimeField, Zero}; -use ark_r1cs_std::{eq::EqGadget, fields::fp::FpVar}; -use ark_relations::gr1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode, -}; +use ark_relations::gr1cs::{ConstraintSystem, SynthesisError, SynthesisMode}; use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_fs::{ - FoldingInstance, FoldingInstanceVar, FoldingScheme, FoldingSchemeFullGadget, - FoldingSchemePartialGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, + FoldingInstance, FoldingScheme, FoldingSchemeFullGadget, FoldingSchemePartialGadget, + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ algebra::field::emulated::EmulatedFieldVar, @@ -30,7 +25,7 @@ use crate::{ pub mod circuits; -pub trait FoldingSchemeCycleFoldGadget: +pub trait FoldingSchemeCycleFoldExt: GroupBasedFoldingSchemePrimary { type CFConfig: CycleFoldConfig::Commitment>; @@ -107,7 +102,7 @@ pub struct CycleFoldBasedIVC { impl IVC for CycleFoldBasedIVC where - FS1: FoldingSchemeCycleFoldGadget< + FS1: FoldingSchemeCycleFoldExt< 1, 1, Arith: From::Commitment>>>, @@ -255,7 +250,7 @@ where FS1::prove(pk1, &mut transcript, &[W], &[U], &[w], &[u], &mut rng)?; let cf_configs = FS1::to_cyclefold_configs(&[U], &[u], &proof, challenge); - for cfg in cf_configs { + for (i, cfg) in cf_configs.iter().enumerate() { let cs = AssignmentsExtractor::new(); cs.execute_fn(|cs| cfg.verify_point_rlc(cs))?; @@ -265,8 +260,8 @@ where (cf_WW, cf_UU, cf_proof, _) = FS2::prove( pk2, &mut transcript, - &[cf_W], - &[cf_U], + &[if i == 0 { cf_W } else { &cf_WW }], + &[if i == 0 { cf_U } else { &cf_UU }], &[&cf_w], &[&cf_u], &mut rng, From 2cb766f29c4740615a18473dd1d0a2be4399ea7e Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 18 Nov 2025 03:04:37 +0800 Subject: [PATCH 44/93] Prefer ark_std over std --- crates/fs/src/lib.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index e1bc7e0fb..b561fcead 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -1,4 +1,3 @@ -use std::ops::{Deref, DerefMut}; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ @@ -9,7 +8,12 @@ use ark_r1cs_std::{ select::CondSelectGadget, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{borrow::Borrow, fmt::Debug, rand::RngCore}; +use ark_std::{ + borrow::Borrow, + fmt::Debug, + ops::{Deref, DerefMut}, + rand::RngCore, +}; use sonobe_primitives::{ arithmetizations::{Arith, ArithConfig}, circuits::AssignmentsOwned, @@ -475,7 +479,7 @@ mod tests { use sonobe_primitives::{ circuits::{ArithExtractor, AssignmentsOwned}, transcripts::{ - griffin::{GriffinParams, sponge::GriffinSponge}, + griffin::{sponge::GriffinSponge, GriffinParams}, poseidon::poseidon_canonical_config, }, }; From 86a9415f424b767e945e9818578d7a14fb210e9a Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 18 Nov 2025 08:30:31 +0800 Subject: [PATCH 45/93] Decider key now contains pk and vk --- crates/fs/src/lib.rs | 73 +++++++++--------- .../ivc/src/compilers/cyclefold/circuits.rs | 5 +- crates/ivc/src/compilers/cyclefold/mod.rs | 74 ++++++++----------- 3 files changed, 72 insertions(+), 80 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index b561fcead..25be38ef8 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -1,4 +1,3 @@ - use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ GR1CSVar, @@ -260,7 +259,15 @@ impl FoldingInstance for PlainInstance { } } -pub trait DeciderKey {} +pub trait DeciderKey { + type ProverKey; + type VerifierKey; + type ArithConfig: ArithConfig; + + fn to_pk(&self) -> &Self::ProverKey; + fn to_vk(&self) -> &Self::VerifierKey; + fn to_arith_config(&self) -> &Self::ArithConfig; +} pub trait FoldingScheme { type VC: CommitmentDef; @@ -269,12 +276,11 @@ pub trait FoldingScheme { type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; type TranscriptField: SonobeField; - type Arith: Arith; + type Arith: Arith::ArithConfig>; type Config; type PublicParam; - type ProverKey; - type VerifierKey; - type DeciderKey: Clone + type DeciderKey: DeciderKey + + Clone + Relation + Relation + WitnessInstanceSampler @@ -300,10 +306,7 @@ pub trait FoldingScheme { /// The key generation method is a deterministic algorithm that takes as /// input the public parameters `pp` and the constraint system `arith`, and /// outputs a prover key and a verifier key. - fn generate_keys( - pp: Self::PublicParam, - arith: Self::Arith, - ) -> Result<(Self::ProverKey, Self::VerifierKey, Self::DeciderKey), Error>; + fn generate_keys(pp: Self::PublicParam, arith: Self::Arith) -> Result; /// The proof generation method is a deterministic algorithm that takes as /// input the prover key `pk`, the transcript `transcript` between the @@ -316,7 +319,7 @@ pub trait FoldingScheme { /// circuits in our CycleFold-based folding-to-IVC compiler. #[allow(non_snake_case)] fn prove( - pk: &Self::ProverKey, + pk: &::ProverKey, transcript: &mut impl Transcript, Ws: &[impl Borrow; M], Us: &[impl Borrow; M], @@ -327,7 +330,7 @@ pub trait FoldingScheme { #[allow(non_snake_case)] fn verify( - vk: &Self::VerifierKey, + vk: &::VerifierKey, transcript: &mut impl Transcript, Us: &[impl Borrow; M], us: &[impl Borrow; N], @@ -397,34 +400,34 @@ impl FoldingInstanceVar for PlainInstanceVar { pub trait GroupBasedFoldingSchemePrimary: FoldingScheme< - M, - N, - VC: GroupBasedCommitment, - TranscriptField = <>::VC as CommitmentDef>::Scalar, -> -{ - type Gadget: FoldingSchemePartialGadget< M, N, - Native = Self, - VC = ::Gadget2, - >; + VC: GroupBasedCommitment, + TranscriptField = <>::VC as CommitmentDef>::Scalar, + > +{ + type Gadget: FoldingSchemePartialGadget< + M, + N, + Native = Self, + VC = ::Gadget2, + >; } pub trait GroupBasedFoldingSchemeSecondary: FoldingScheme< - M, - N, - VC: GroupBasedCommitment, - TranscriptField = CF2<<>::VC as CommitmentDef>::Commitment>, -> -{ - type Gadget: FoldingSchemeFullGadget< M, N, - Native = Self, - VC = ::Gadget1, - >; + VC: GroupBasedCommitment, + TranscriptField = CF2<<>::VC as CommitmentDef>::Commitment>, + > +{ + type Gadget: FoldingSchemeFullGadget< + M, + N, + Native = Self, + VC = ::Gadget1, + >; } pub trait FoldingSchemePartialGadget { @@ -479,7 +482,7 @@ mod tests { use sonobe_primitives::{ circuits::{ArithExtractor, AssignmentsOwned}, transcripts::{ - griffin::{sponge::GriffinSponge, GriffinParams}, + griffin::{GriffinParams, sponge::GriffinSponge}, poseidon::poseidon_canonical_config, }, }; @@ -501,7 +504,9 @@ mod tests { let cs = ArithExtractor::new(); cs.execute_synthesizer(circuit)?; let arith = cs.arith()?; - let (pk, vk, dk) = FS::generate_keys(pp, arith)?; + let dk = FS::generate_keys(pp, arith)?; + let pk = dk.to_pk(); + let vk = dk.to_vk(); let mut Ws = vec![]; let mut Us = vec![]; diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index 291f3c532..ab6fda74c 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -3,6 +3,7 @@ use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, boolean::Boolean, + convert::ToConstraintFieldGadget, eq::EqGadget, fields::{FieldVar, fp::FpVar}, groups::CurveVar, @@ -241,9 +242,7 @@ pub trait CycleFoldConfig: Sized + Default { /// The final vector of public inputs is shorter than the result of calling /// [`AllocVar::new_input`], because we only need the x and y coordinates of /// the point, but the `infinity` flag is not necessary. - fn mark_point_as_public( - point: &impl CurveVar>, - ) -> Result<(), SynthesisError> { + fn mark_point_as_public(point: &::Var) -> Result<(), SynthesisError> { for x in &point.to_constraint_field()?[..2] { // This line "converts" `x` from a witness to a public input. // Instead of directly modifying the constraint system, we explicitly diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 5a91923d4..e8d51a55d 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -2,8 +2,8 @@ use ark_ff::{PrimeField, Zero}; use ark_relations::gr1cs::{ConstraintSystem, SynthesisError, SynthesisMode}; use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_fs::{ - FoldingInstance, FoldingScheme, FoldingSchemeFullGadget, FoldingSchemePartialGadget, - GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, + DeciderKey, FoldingInstance, FoldingScheme, FoldingSchemeFullGadget, + FoldingSchemePartialGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ algebra::field::emulated::EmulatedFieldVar, @@ -58,15 +58,11 @@ pub trait FoldingSchemeCycleFoldExt: >; } -pub struct ProverKey, FS2: FoldingScheme<1, 1>>( - FS1::ProverKey, +pub struct Key, FS2: FoldingScheme<1, 1>>( FS1::DeciderKey, - FS2::ProverKey, FS2::DeciderKey, Arc>, F, - ::Config, - ::Config, ); pub struct Proof, FS2: FoldingScheme<1, 1>>( @@ -78,12 +74,12 @@ pub struct Proof, FS2: FoldingScheme<1, 1>>( FS2::RU, ); -impl, FS2: FoldingScheme<1, 1>> - Dummy<&ProverKey> for Proof +impl, FS2: FoldingScheme<1, 1>> Dummy<&Key> + for Proof { - fn dummy(pk: &ProverKey) -> Self { - let cfg1 = &pk.6; - let cfg2 = &pk.7; + fn dummy(pk: &Key) -> Self { + let cfg1 = pk.0.to_arith_config(); + let cfg2 = pk.1.to_arith_config(); let W = FS1::RW::dummy(cfg1); let U = FS1::RU::dummy(cfg1); @@ -131,14 +127,9 @@ where Arc>, ); - type ProverKey = ProverKey; + type ProverKey = Key; - type VerifierKey = ( - FS1::DeciderKey, - FS2::DeciderKey, - Arc>, - Self::Field, - ); + type VerifierKey = Key; type Proof = Proof; @@ -190,31 +181,19 @@ where arith1 = new_arith1; } - let arith1_config = arith1.config().clone(); - let arith2_config = arith2.config().clone(); - - let (pk1, _, dk1) = FS1::generate_keys(pp1, arith1)?; - let (pk2, _, dk2) = FS2::generate_keys(pp2, arith2)?; + let dk1 = FS1::generate_keys(pp1, arith1)?; + let dk2 = FS2::generate_keys(pp2, arith2)?; let pp_hash = Zero::zero(); // TODO Ok(( - ProverKey( - pk1, - dk1.clone(), - pk2, - dk2.clone(), - griffin_config.clone(), - pp_hash, - arith1_config, - arith2_config, - ), - (dk1, dk2, griffin_config, pp_hash), + Key(dk1.clone(), dk2.clone(), griffin_config.clone(), pp_hash), + Key(dk1, dk2, griffin_config, pp_hash), )) } fn prove>( - ProverKey(pk1, dk1, pk2, dk2, griffin_config, pp_hash, arith1_config, arith2_config): &Self::ProverKey, + Key(dk1, dk2, griffin_config, pp_hash): &Self::ProverKey, step_circuit: &FC, i: usize, initial_state: &FC::State, @@ -226,6 +205,8 @@ where let hash = GriffinSponge::new_with_pp_hash(&griffin_config, *pp_hash); let mut transcript = hash.separate_domain("transcript".as_ref()); + let arith1_config = dk1.to_arith_config(); + let arith2_config = dk2.to_arith_config(); let augmented_circuit = AugmentedCircuit:: { griffin_config: griffin_config.clone(), arith1_config, @@ -246,8 +227,15 @@ where cf_proofs.clear(); let challenge; - (WW, UU, proof, challenge) = - FS1::prove(pk1, &mut transcript, &[W], &[U], &[w], &[u], &mut rng)?; + (WW, UU, proof, challenge) = FS1::prove( + dk1.to_pk(), + &mut transcript, + &[W], + &[U], + &[w], + &[u], + &mut rng, + )?; let cf_configs = FS1::to_cyclefold_configs(&[U], &[u], &proof, challenge); for (i, cfg) in cf_configs.iter().enumerate() { @@ -258,7 +246,7 @@ where let cf_proof; (cf_WW, cf_UU, cf_proof, _) = FS2::prove( - pk2, + dk2.to_pk(), &mut transcript, &[if i == 0 { cf_W } else { &cf_WW }], &[if i == 0 { cf_U } else { &cf_UU }], @@ -299,7 +287,7 @@ where } fn verify>( - (dk1, dk2, griffin_config, pp_hash): &Self::VerifierKey, + Key(dk1, dk2, griffin_config, pp_hash): &Self::VerifierKey, i: usize, initial_state: &FC::State, current_state: &FC::State, @@ -326,9 +314,9 @@ where return Err(Error::IVCVerificationFail); } - FS1::decide_running(&dk1, &W, &U)?; - FS1::decide_incoming(&dk1, &w, &u)?; - FS2::decide_running(&dk2, &cf_W, &cf_U)?; + FS1::decide_running(dk1, W, U)?; + FS1::decide_incoming(dk1, w, u)?; + FS2::decide_running(dk2, cf_W, cf_U)?; Ok(()) } From 867220a6c2fca93f33b8c9471880d57f3eecf1fc Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 04:50:00 +0800 Subject: [PATCH 46/93] Use generic hash function in CF --- crates/fs/src/lib.rs | 6 ++ .../ivc/src/compilers/cyclefold/circuits.rs | 47 ++------- crates/ivc/src/compilers/cyclefold/mod.rs | 98 ++++++++----------- 3 files changed, 55 insertions(+), 96 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 25be38ef8..0d812dc39 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -38,6 +38,12 @@ pub enum Error { Unsupported(String), #[error("Failed to create domain")] DomainCreationFailure, + #[error("Indivisible by vanishing polynomial")] + IndivisibleByVanishingPoly, + #[error("Unsatisfied relation: {0}")] + UnsatisfiedRelation(String), + #[error("Invalid public parameters: {0}")] + InvalidPublicParameters(String), } pub trait FoldingWitness: Debug { diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index ab6fda74c..c93c4ff0e 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -9,7 +9,7 @@ use ark_r1cs_std::{ groups::CurveVar, }; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::{marker::PhantomData, sync::Arc}; +use ark_std::marker::PhantomData; use sonobe_fs::{ FoldingInstanceVar, FoldingSchemeFullGadget, FoldingSchemePartialGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, @@ -34,14 +34,15 @@ pub struct AugmentedCircuit< FS1: GroupBasedFoldingSchemePrimary<1, 1>, FS2: GroupBasedFoldingSchemeSecondary<1, 1>, FC: FCircuit, + T: Transcript, > { - pub griffin_config: Arc>, + pub hash_config: T::Config, pub arith1_config: &'a ::Config, pub arith2_config: &'a ::Config, pub step_circuit: &'a FC, } -impl<'a, FS1, FS2, FC> AugmentedCircuit<'a, FS1, FS2, FC> +impl<'a, FS1, FS2, FC, T> AugmentedCircuit<'a, FS1, FS2, FC, T> where FS1: FoldingSchemeCycleFoldExt< 1, @@ -60,6 +61,7 @@ where >, >, FC: FCircuit::Scalar>, + T: Transcript, { pub fn compute_next_state( &self, @@ -76,8 +78,8 @@ where cf_us: Vec, cf_proofs: Vec, ) -> Result<(FC::State, FC::ExternalOutputs), SynthesisError> { - let hash = GriffinSpongeVar::new_with_pp_hash( - &self.griffin_config, + let hash = T::Gadget::new_with_pp_hash( + &self.hash_config, &FpVar::new_witness(cs.clone(), || Ok(pp_hash))?, )?; let sponge = hash.separate_domain("sponge".as_ref())?; @@ -156,7 +158,7 @@ where } } -impl<'a, FS1, FS2, FC> ConstraintSynthesizer for AugmentedCircuit<'a, FS1, FS2, FC> +impl<'a, FS1, FS2, FC, T> ConstraintSynthesizer for AugmentedCircuit<'a, FS1, FS2, FC, T> where FS1: FoldingSchemeCycleFoldExt< 1, @@ -173,6 +175,7 @@ where VC: CommitmentDef>, >, FC: FCircuit::Scalar>, + T: Transcript, { fn generate_constraints( self, @@ -205,38 +208,6 @@ where pub trait CycleFoldConfig: Sized + Default { type C: SonobeCurve; - /// `N_INPUT_POINTS` specifies the number of input points that are folded in - /// [`CycleFoldCircuit`] via random linear combinations. - const N_INPUT_POINTS: usize; - const N_INPUT_RANDOMNESS_BITS: usize; - /// `FIELD_CAPACITY` is the maximum number of bits that can be stored in a - /// field element. - /// - /// By default, `FIELD_CAPACITY` is set to `MODULUS_BIT_SIZE - 1`. - /// - /// Given a randomness with `N_INPUT_RANDOMNESS_BITS` bits, we need - /// `N_INPUT_RANDOMNESS_BITS / FIELD_CAPACITY` field elements to pack it - /// *compactly* in-circuit. - const FIELD_CAPACITY: usize = CF2::::MODULUS_BIT_SIZE as usize - 1; - - /// Public inputs length for the [`CycleFoldCircuit`], which depends on the - /// above constants defined by the concrete folding scheme. For example: - /// * In Nova, this is `|r| + |p_1| + |p_2| + |P|` - /// * In HyperNova, this is `|r| + |p_i| * n_points + |P|`. - /// * In ProtoGalaxy, this is `|[..., r_i, ...]| + |p_i| * n_points + |P|`. - /// - /// As explained above, `|r|` (i.e., the length of a single randomness) is - /// `N_INPUT_RANDOMNESS_BITS / FIELD_CAPACITY`. - /// The length of a point `p_i` when treated as public inputs is 2, as we - /// only need the `x` and `y` coordinates of the point. - /// - /// Thus, `IO_LEN` is: - /// `N_INPUT_RANDOMNESS_BITS / FIELD_CAPACITY + 2 * (N_INPUT_POINTS + 1)`. - const IO_LEN: usize = { - Self::N_INPUT_RANDOMNESS_BITS.div_ceil(Self::FIELD_CAPACITY) - + 2 * (Self::N_INPUT_POINTS + 1) - }; - /// `mark_point_as_public` marks a point as public. /// /// The final vector of public inputs is shorter than the result of calling diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index e8d51a55d..aebe497d0 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -58,11 +58,10 @@ pub trait FoldingSchemeCycleFoldExt: >; } -pub struct Key, FS2: FoldingScheme<1, 1>>( +pub struct Key, FS2: FoldingScheme<1, 1>, T>( FS1::DeciderKey, FS2::DeciderKey, - Arc>, - F, + T, ); pub struct Proof, FS2: FoldingScheme<1, 1>>( @@ -74,29 +73,28 @@ pub struct Proof, FS2: FoldingScheme<1, 1>>( FS2::RU, ); -impl, FS2: FoldingScheme<1, 1>> Dummy<&Key> +impl, FS2: FoldingScheme<1, 1>, T> Dummy<&Key> for Proof { - fn dummy(pk: &Key) -> Self { + fn dummy(pk: &Key) -> Self { let cfg1 = pk.0.to_arith_config(); let cfg2 = pk.1.to_arith_config(); - - let W = FS1::RW::dummy(cfg1); - let U = FS1::RU::dummy(cfg1); - let w = FS1::IW::dummy(cfg1); - let u = FS1::IU::dummy(cfg1); - let cf_W = FS2::RW::dummy(cfg2); - let cf_U = FS2::RU::dummy(cfg2); - - Self(W, U, w, u, cf_W, cf_U) + Self( + FS1::RW::dummy(cfg1), + FS1::RU::dummy(cfg1), + FS1::IW::dummy(cfg1), + FS1::IU::dummy(cfg1), + FS2::RW::dummy(cfg2), + FS2::RU::dummy(cfg2), + ) } } -pub struct CycleFoldBasedIVC { - _d: PhantomData<(FS1, FS2)>, +pub struct CycleFoldBasedIVC { + _d: PhantomData<(FS1, FS2, T)>, } -impl IVC for CycleFoldBasedIVC +impl IVC for CycleFoldBasedIVC where FS1: FoldingSchemeCycleFoldExt< 1, @@ -116,57 +114,46 @@ where Commitment: SonobeCurve::Scalar>, >, >, + T: Transcript::Commitment>>, { type Field = ::Scalar; - type Config = (FS1::Config, FS2::Config, Arc>); + type Config = (FS1::Config, FS2::Config, T::Config); - type PublicParam = ( - FS1::PublicParam, - FS2::PublicParam, - Arc>, - ); + type PublicParam = (FS1::PublicParam, FS2::PublicParam, T::Config); - type ProverKey = Key; + type ProverKey = Key; - type VerifierKey = Key; + type VerifierKey = Key; type Proof = Proof; fn preprocess( - (cfg1, cfg2, griffin_config): Self::Config, + (cfg1, cfg2, hash_config): Self::Config, mut rng: impl RngCore, ) -> Result { Ok(( FS1::preprocess(cfg1, &mut rng)?, FS2::preprocess(cfg2, &mut rng)?, - griffin_config, + hash_config, )) } fn generate_keys>( - (pp1, pp2, griffin_config): Self::PublicParam, + (pp1, pp2, hash_config): Self::PublicParam, step_circuit: &FC, ) -> Result<(Self::ProverKey, Self::VerifierKey), Error> { - let mut arith2 = FS2::Arith::default(); + let cyclefold_circuit = CycleFoldCircuit::::default(); - loop { - let cyclefold_circuit = CycleFoldCircuit::::default(); - - let cs = ArithExtractor::new(); - cs.execute_synthesizer(cyclefold_circuit)?; - let new_arith2 = cs.arith::()?; - if new_arith2.config() == arith2.config() { - break; - } - arith2 = new_arith2; - } + let cs = ArithExtractor::new(); + cs.execute_synthesizer(cyclefold_circuit)?; + let arith2 = cs.arith::()?; let mut arith1 = FS1::Arith::default(); loop { - let augmented_circuit = AugmentedCircuit:: { - griffin_config: griffin_config.clone(), + let augmented_circuit = AugmentedCircuit:: { + hash_config: hash_config.clone(), arith1_config: arith1.config(), arith2_config: arith2.config(), step_circuit, @@ -187,13 +174,13 @@ where let pp_hash = Zero::zero(); // TODO Ok(( - Key(dk1.clone(), dk2.clone(), griffin_config.clone(), pp_hash), - Key(dk1, dk2, griffin_config, pp_hash), + Key(dk1.clone(), dk2.clone(), (hash_config.clone(), pp_hash)), + Key(dk1, dk2, (hash_config, pp_hash)), )) } fn prove>( - Key(dk1, dk2, griffin_config, pp_hash): &Self::ProverKey, + Key(dk1, dk2, (hash_config, pp_hash)): &Self::ProverKey, step_circuit: &FC, i: usize, initial_state: &FC::State, @@ -202,13 +189,13 @@ where Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, mut rng: impl RngCore, ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error> { - let hash = GriffinSponge::new_with_pp_hash(&griffin_config, *pp_hash); + let hash = T::new_with_pp_hash(&hash_config, *pp_hash); let mut transcript = hash.separate_domain("transcript".as_ref()); let arith1_config = dk1.to_arith_config(); let arith2_config = dk2.to_arith_config(); - let augmented_circuit = AugmentedCircuit:: { - griffin_config: griffin_config.clone(), + let augmented_circuit = AugmentedCircuit:: { + hash_config: hash_config.clone(), arith1_config, arith2_config, step_circuit, @@ -223,9 +210,6 @@ where let mut cf_WW = Dummy::dummy(arith2_config); if i != 0 { - cf_us.clear(); - cf_proofs.clear(); - let challenge; (WW, UU, proof, challenge) = FS1::prove( dk1.to_pk(), @@ -244,8 +228,7 @@ where let (cf_w, cf_u) = dk2.sample(cs.assignments()?, &mut rng)?; - let cf_proof; - (cf_WW, cf_UU, cf_proof, _) = FS2::prove( + (cf_WW, cf_UU, cf_proofs[i], _) = FS2::prove( dk2.to_pk(), &mut transcript, &[if i == 0 { cf_W } else { &cf_WW }], @@ -254,8 +237,7 @@ where &[&cf_u], &mut rng, )?; - cf_us.push(cf_u); - cf_proofs.push(cf_proof); + cf_us[i] = cf_u; } } @@ -287,7 +269,7 @@ where } fn verify>( - Key(dk1, dk2, griffin_config, pp_hash): &Self::VerifierKey, + Key(dk1, dk2, (hash_config, pp_hash)): &Self::VerifierKey, i: usize, initial_state: &FC::State, current_state: &FC::State, @@ -299,8 +281,8 @@ where .ok_or(Error::IVCVerificationFail); } - let griffin = GriffinSponge::new_with_pp_hash(griffin_config, *pp_hash); - let mut sponge = griffin.separate_domain("sponge".as_ref()); + let hash = T::new_with_pp_hash(hash_config, *pp_hash); + let mut sponge = hash.separate_domain("sponge".as_ref()); let u_x = sponge .add(&i) From ea6cb018ddd90aa14e0c56797c8f41ff615e4ee7 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 04:50:26 +0800 Subject: [PATCH 47/93] Initial redesign of decider trait --- crates/ivc/src/lib.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index 88bd3b592..cd1e51a87 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -101,6 +101,31 @@ impl, I: IVC> IVCStatefulProver { } } +pub trait Decider { + type IVC: IVC; + + type ProverKey; + type VerifierKey; + type Instance; + type Witness; + type Proof; + + fn preprocess_and_generate_keys( + ivc_pk: &::ProverKey, + rng: impl RngCore, + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + + fn prove( + pk: &Self::ProverKey, + w: &Self::Witness, + x: &Self::Instance, + rng: impl RngCore, + ) -> Result; + + fn verify(vk: &Self::VerifierKey, x: &Self::Instance, proof: &Self::Proof) + -> Result<(), Error>; +} + #[cfg(test)] mod tests { use ark_bn254::{Fr, G1Projective as C1}; From 09b8075718b5d1939d63fe54b26e456cee461797 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 20:14:48 +0800 Subject: [PATCH 48/93] Introduce tagged vector for plain instance & witness --- crates/fs/src/lib.rs | 136 +++++++++---------------------------------- 1 file changed, 28 insertions(+), 108 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 0d812dc39..2386e6456 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -66,9 +66,9 @@ pub trait FoldingInstance: Clone + Debug + PartialEq + Absorb } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct PlainWitness(pub Vec); +pub struct TaggedVec(pub Vec); -impl Deref for PlainWitness { +impl Deref for TaggedVec { type Target = Vec; fn deref(&self) -> &Self::Target { @@ -76,42 +76,48 @@ impl Deref for PlainWitness { } } -impl DerefMut for PlainWitness { +impl DerefMut for TaggedVec { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl From> for PlainWitness { +impl From> for TaggedVec { fn from(v: Vec) -> Self { Self(v) } } -impl Absorbable for PlainWitness { +impl Absorbable for TaggedVec { fn absorb_into(&self, dest: &mut Vec) { self.0.absorb_into(dest) } } -impl> AbsorbableVar for PlainWitness { +impl, const TAG: char> AbsorbableVar + for TaggedVec +{ fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.0.absorb_into(dest) } } -impl, Y, F: Field> AllocVar, F> for PlainWitness { - fn new_variable>>( +impl, Y, const TAG: char> AllocVar, F> + for TaggedVec +{ + fn new_variable>>( cs: impl Into>, f: impl FnOnce() -> Result, mode: AllocationMode, ) -> Result { let v = f()?; - Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(|v| Self(v)) + Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(Self) } } -impl> CondSelectGadget for PlainWitness { +impl, const TAG: char> CondSelectGadget + for TaggedVec +{ fn conditionally_select( cond: &Boolean, true_value: &Self, @@ -120,29 +126,29 @@ impl> CondSelectGadget for PlainWitness if true_value.len() != false_value.len() { return Err(SynthesisError::Unsatisfiable); } - Ok(Self( - true_value - .0 - .iter() - .zip(false_value.0.iter()) - .map(|(t, f)| cond.select(t, f)) - .collect::>()?, - )) + true_value + .iter() + .zip(false_value.iter()) + .map(|(t, f)| cond.select(t, f)) + .collect::>() + .map(Self) } } -impl> GR1CSVar for PlainWitness { - type Value = PlainWitness; +impl, const TAG: char> GR1CSVar for TaggedVec { + type Value = TaggedVec; fn cs(&self) -> ConstraintSystemRef { self.0.cs() } fn value(&self) -> Result { - self.0.value().map(PlainWitness) + self.0.value().map(TaggedVec) } } +pub type PlainWitness = TaggedVec; + impl Dummy<&A> for PlainWitness { fn dummy(cfg: &A) -> Self { vec![V::default(); cfg.n_witnesses()].into() @@ -157,83 +163,7 @@ impl FoldingWitness for PlainWitness { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PlainInstance(pub Vec); - -impl Deref for PlainInstance { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for PlainInstance { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From> for PlainInstance { - fn from(v: Vec) -> Self { - Self(v) - } -} - -impl Absorbable for PlainInstance { - fn absorb_into(&self, dest: &mut Vec) { - self.0.absorb_into(dest) - } -} - -impl> AbsorbableVar for PlainInstance { - fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { - self.0.absorb_into(dest) - } -} - -impl, Y, F: Field> AllocVar, F> for PlainInstance { - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let v = f()?; - Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(|v| Self(v)) - } -} - -impl> CondSelectGadget for PlainInstance { - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - if true_value.len() != false_value.len() { - return Err(SynthesisError::Unsatisfiable); - } - Ok(Self( - true_value - .0 - .iter() - .zip(false_value.0.iter()) - .map(|(t, f)| cond.select(t, f)) - .collect::>()?, - )) - } -} - -impl> GR1CSVar for PlainInstance { - type Value = PlainInstance; - - fn cs(&self) -> ConstraintSystemRef { - self.0.cs() - } - - fn value(&self) -> Result { - self.0.value().map(PlainInstance) - } -} +pub type PlainInstance = TaggedVec; impl Dummy<&A> for PlainInstance { fn dummy(cfg: &A) -> Self { @@ -241,14 +171,6 @@ impl Dummy<&A> for PlainInstance { } } -impl FoldingWitness for PlainInstance { - const N_OPENINGS: usize = 0; - - fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { - vec![] - } -} - impl FoldingInstance for PlainInstance { const N_COMMITMENTS: usize = 0; @@ -440,9 +362,7 @@ pub trait FoldingSchemePartialGadget { type Native: FoldingScheme; type VC: CommitmentDefGadget>::VC>; - type RW: FoldingWitnessVar>::RW>; type RU: FoldingInstanceVar>::RU>; - type IW: FoldingWitnessVar>::IW>; type IU: FoldingInstanceVar>::IU>; type VerifierKey; From 0d6f6a13185eb20adfcda96ca4d6362e23f09dd8 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 20:15:37 +0800 Subject: [PATCH 49/93] Make fields of CF key and proof visible --- crates/ivc/src/compilers/cyclefold/mod.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index aebe497d0..4accd00f1 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -59,18 +59,18 @@ pub trait FoldingSchemeCycleFoldExt: } pub struct Key, FS2: FoldingScheme<1, 1>, T>( - FS1::DeciderKey, - FS2::DeciderKey, - T, + pub FS1::DeciderKey, + pub FS2::DeciderKey, + pub T, ); pub struct Proof, FS2: FoldingScheme<1, 1>>( - FS1::RW, - FS1::RU, - FS1::IW, - FS1::IU, - FS2::RW, - FS2::RU, + pub FS1::RW, + pub FS1::RU, + pub FS1::IW, + pub FS1::IU, + pub FS2::RW, + pub FS2::RU, ); impl, FS2: FoldingScheme<1, 1>, T> Dummy<&Key> From 5b33e690841584dbc7eae0048eb2a081d5be3acb Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 22:33:18 +0800 Subject: [PATCH 50/93] Allow step circuit to have states of any shape --- .../ivc/src/compilers/cyclefold/circuits.rs | 48 +++++++++---------- crates/ivc/src/compilers/cyclefold/mod.rs | 20 ++++---- crates/ivc/src/lib.rs | 33 +++++++------ 3 files changed, 50 insertions(+), 51 deletions(-) diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index c93c4ff0e..cbe95b319 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -45,21 +45,21 @@ pub struct AugmentedCircuit< impl<'a, FS1, FS2, FC, T> AugmentedCircuit<'a, FS1, FS2, FC, T> where FS1: FoldingSchemeCycleFoldExt< - 1, - 1, - Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, - >, + 1, + 1, + Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, + >, FS2: GroupBasedFoldingSchemeSecondary< - 1, - 1, - Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, - >, + 1, + 1, + Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, + >, FC: FCircuit::Scalar>, T: Transcript, { @@ -161,19 +161,19 @@ where impl<'a, FS1, FS2, FC, T> ConstraintSynthesizer for AugmentedCircuit<'a, FS1, FS2, FC, T> where FS1: FoldingSchemeCycleFoldExt< - 1, - 1, - Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, - >, + 1, + 1, + Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, + >, FS2: GroupBasedFoldingSchemeSecondary< - 1, - 1, - Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef>, - >, + 1, + 1, + Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + VC: CommitmentDef>, + >, FC: FCircuit::Scalar>, T: Transcript, { @@ -230,7 +230,7 @@ pub trait CycleFoldConfig: Sized + Default { } fn verify_point_rlc(&self, cs: ConstraintSystemRef>) - -> Result<(), SynthesisError>; + -> Result<(), SynthesisError>; } #[derive(Debug, Clone, Default)] diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 4accd00f1..8793b979c 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -122,11 +122,11 @@ where type PublicParam = (FS1::PublicParam, FS2::PublicParam, T::Config); - type ProverKey = Key; + type ProverKey = Key; - type VerifierKey = Key; + type VerifierKey = Key; - type Proof = Proof; + type Proof = Proof; fn preprocess( (cfg1, cfg2, hash_config): Self::Config, @@ -142,7 +142,7 @@ where fn generate_keys>( (pp1, pp2, hash_config): Self::PublicParam, step_circuit: &FC, - ) -> Result<(Self::ProverKey, Self::VerifierKey), Error> { + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error> { let cyclefold_circuit = CycleFoldCircuit::::default(); let cs = ArithExtractor::new(); @@ -180,16 +180,16 @@ where } fn prove>( - Key(dk1, dk2, (hash_config, pp_hash)): &Self::ProverKey, + Key(dk1, dk2, (hash_config, pp_hash)): &Self::ProverKey, step_circuit: &FC, i: usize, initial_state: &FC::State, current_state: &FC::State, external_inputs: FC::ExternalInputs, - Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, + Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, mut rng: impl RngCore, - ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error> { - let hash = T::new_with_pp_hash(&hash_config, *pp_hash); + ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error> { + let hash = T::new_with_pp_hash(hash_config, *pp_hash); let mut transcript = hash.separate_domain("transcript".as_ref()); let arith1_config = dk1.to_arith_config(); @@ -269,11 +269,11 @@ where } fn verify>( - Key(dk1, dk2, (hash_config, pp_hash)): &Self::VerifierKey, + Key(dk1, dk2, (hash_config, pp_hash)): &Self::VerifierKey, i: usize, initial_state: &FC::State, current_state: &FC::State, - Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, + Proof(W, U, w, u, cf_W, cf_U): &Self::Proof, ) -> Result<(), Error> { if i == 0 { return (initial_state == current_state) diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index cd1e51a87..7a660a54e 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -23,49 +23,49 @@ pub trait IVC { type Config; type PublicParam; - type ProverKey; - type VerifierKey; - type Proof: for<'a> Dummy<&'a Self::ProverKey>; + type ProverKey; + type VerifierKey; + type Proof: for<'a> Dummy<&'a Self::ProverKey>; fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; fn generate_keys>( pp: Self::PublicParam, step_circuit: &FC, - ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; fn prove>( - pk: &Self::ProverKey, + pk: &Self::ProverKey, step_circuit: &FC, i: usize, initial_state: &FC::State, current_state: &FC::State, external_inputs: FC::ExternalInputs, - current_proof: &Self::Proof, + current_proof: &Self::Proof, rng: impl RngCore, - ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error>; + ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error>; fn verify>( - vk: &Self::VerifierKey, + vk: &Self::VerifierKey, i: usize, initial_state: &FC::State, current_state: &FC::State, - proof: &Self::Proof, + proof: &Self::Proof, ) -> Result<(), Error>; } pub struct IVCStatefulProver { - pub pk: I::ProverKey, + pub pk: I::ProverKey, pub step_circuit: FC, pub i: usize, pub initial_state: FC::State, pub current_state: FC::State, - pub current_proof: I::Proof, + pub current_proof: I::Proof, } impl, I: IVC> IVCStatefulProver { pub fn new( - pk: I::ProverKey, + pk: I::ProverKey, step_circuit: FC, initial_state: FC::State, ) -> Result { @@ -110,8 +110,8 @@ pub trait Decider { type Witness; type Proof; - fn preprocess_and_generate_keys( - ivc_pk: &::ProverKey, + fn preprocess_and_generate_keys( + ivc_pk: &::ProverKey, rng: impl RngCore, ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; @@ -123,14 +123,13 @@ pub trait Decider { ) -> Result; fn verify(vk: &Self::VerifierKey, x: &Self::Instance, proof: &Self::Proof) - -> Result<(), Error>; + -> Result<(), Error>; } #[cfg(test)] mod tests { use ark_bn254::{Fr, G1Projective as C1}; use ark_crypto_primitives::sponge::{CryptographicSponge, poseidon::PoseidonSponge}; - use ark_ff::UniformRand; use ark_grumpkin::Projective as C2; use ark_std::{error::Error, rand::Rng, sync::Arc, test_rng}; use sonobe_fs::{ @@ -164,7 +163,7 @@ mod tests { for external_inputs in external_inputs_vec { prover.prove_step(external_inputs, &mut rng)?; - I::verify::( + I::verify( &vk, prover.i, &prover.initial_state, From d63173f556d44df24e9f5e549ff5e732a6c53685 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 23:02:25 +0800 Subject: [PATCH 51/93] We only need 1 public input for primary instances in CF --- crates/ivc/src/compilers/cyclefold/circuits.rs | 10 +++++----- crates/ivc/src/compilers/cyclefold/mod.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index cbe95b319..616e9bd8e 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -108,8 +108,8 @@ where .add(¤t_state)? .add(&U)? .add(&cf_U)? - .get_field_elements(2)?; - let u = FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), u, u_x)?; + .get_field_element()?; + let u = FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), u, vec![u_x])?; let (UU, rho) = FS1::Gadget::verify_hinted(&(), &mut transcript, [&U], [&u], &proof)?; let actual_UU = is_basecase.select(&U_dummy, &UU)?; @@ -136,7 +136,7 @@ where .add(&next_state)? .add(&actual_UU)? .add(&actual_cf_UU)? - .get_field_elements(2)?; + .get_field_element()?; // This line "converts" `uu_x` from witnesses to public inputs. // Instead of directly modifying the constraint system, we explicitly // allocate a public input and enforce that its value is indeed `uu_x`. @@ -146,8 +146,8 @@ where // computing them outside the circuit. // - `.enforce_equal()` prevents a malicious prover from claiming wrong // public inputs that are not the honest `uu_x` computed in-circuit. - uu_x.enforce_equal(&Vec::new_input(cs.clone(), || { - Ok(uu_x.value().unwrap_or(vec![Default::default(); uu_x.len()])) + uu_x.enforce_equal(&FpVar::new_input(cs.clone(), || { + Ok(uu_x.value().unwrap_or_default()) })?)?; if cs.is_in_setup_mode() { diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 8793b979c..baaa963ae 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -290,9 +290,9 @@ where .add(current_state) .add(U) .add(cf_U) - .get_field_elements(2); + .get_field_element(); - if u.public_inputs() != &u_x[..] { + if u.public_inputs() != [u_x] { return Err(Error::IVCVerificationFail); } From 85b50232ebc09808dacfc251e1dd21e0276e7e46 Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 22 Nov 2025 04:41:39 +0800 Subject: [PATCH 52/93] Separate VC and FS into Def and Ops --- crates/fs/src/lib.rs | 116 +++++++++--------- .../ivc/src/compilers/cyclefold/circuits.rs | 18 +-- crates/ivc/src/compilers/cyclefold/mod.rs | 35 +++--- crates/ivc/src/lib.rs | 4 - 4 files changed, 85 insertions(+), 88 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 2386e6456..0038faf4a 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -94,9 +94,7 @@ impl Absorbable for TaggedVec { } } -impl, const TAG: char> AbsorbableVar - for TaggedVec -{ +impl, const TAG: char> AbsorbableVar for TaggedVec { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.0.absorb_into(dest) } @@ -197,7 +195,7 @@ pub trait DeciderKey { fn to_arith_config(&self) -> &Self::ArithConfig; } -pub trait FoldingScheme { +pub trait FoldingSchemeDef { type VC: CommitmentDef; type RW: FoldingWitness + for<'a> Dummy<&'a ::Config>; type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; @@ -219,8 +217,11 @@ pub trait FoldingScheme { Error = Error, >; type Challenge; - type Proof: Clone + for<'a> Dummy<&'a ::Config>; + type Proof: Clone + + for<'a> Dummy<&'a ::Config>; +} +pub trait FoldingSchemeOps: FoldingSchemeDef { /// The preprocessing method is a randomized algorithm that takes as input /// the size bounds of the folding scheme, which are contained in the /// `config` parameter, and outputs the public parameters. @@ -254,7 +255,7 @@ pub trait FoldingScheme { ws: &[impl Borrow; N], us: &[impl Borrow; N], rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; + ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; #[allow(non_snake_case)] fn verify( @@ -262,7 +263,7 @@ pub trait FoldingScheme { transcript: &mut impl Transcript, Us: &[impl Borrow; M], us: &[impl Borrow; N], - proof: &Self::Proof, + proof: &Self::Proof, ) -> Result; #[allow(non_snake_case)] @@ -305,10 +306,10 @@ pub trait FoldingInstanceVar: ) -> Result; } -pub type PlainWitnessVar = PlainWitness<::ScalarVar>; -pub type PlainInstanceVar = PlainInstance<::ScalarVar>; +pub type PlainWitnessVar = PlainWitness; +pub type PlainInstanceVar = PlainInstance; -impl FoldingInstanceVar for PlainInstanceVar { +impl FoldingInstanceVar for PlainInstanceVar { fn commitments(&self) -> Vec<&VC::CommitmentVar> { vec![] } @@ -326,69 +327,40 @@ impl FoldingInstanceVar for PlainInstanceVar { } } -pub trait GroupBasedFoldingSchemePrimary: - FoldingScheme< - M, - N, - VC: GroupBasedCommitment, - TranscriptField = <>::VC as CommitmentDef>::Scalar, - > -{ - type Gadget: FoldingSchemePartialGadget< - M, - N, - Native = Self, - VC = ::Gadget2, - >; -} +pub trait FoldingSchemeGadgetDef { + type Native: FoldingSchemeDef; -pub trait GroupBasedFoldingSchemeSecondary: - FoldingScheme< - M, - N, - VC: GroupBasedCommitment, - TranscriptField = CF2<<>::VC as CommitmentDef>::Commitment>, - > -{ - type Gadget: FoldingSchemeFullGadget< - M, - N, - Native = Self, - VC = ::Gadget1, - >; -} - -pub trait FoldingSchemePartialGadget { - type Native: FoldingScheme; - - type VC: CommitmentDefGadget>::VC>; - type RU: FoldingInstanceVar>::RU>; - type IU: FoldingInstanceVar>::IU>; + type VC: CommitmentDefGadget::VC>; + type RU: FoldingInstanceVar::RU>; + type IU: FoldingInstanceVar::IU>; type VerifierKey; type Challenge; - - type Proof: AllocVar< - >::Proof, + type Proof: AllocVar< + ::Proof, ::ConstraintField, > + GR1CSVar< ::ConstraintField, - Value = >::Proof, + Value = ::Proof, >; +} +pub trait FoldingSchemeGadgetOpsPartial: + FoldingSchemeGadgetDef> +{ #[allow(non_snake_case)] fn verify_hinted( vk: &Self::VerifierKey, transcript: &mut impl TranscriptGadget<::ConstraintField>, Us: [&Self::RU; M], us: [&Self::IU; N], - proof: &Self::Proof, + proof: &Self::Proof, ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; } -pub trait FoldingSchemeFullGadget: - FoldingSchemePartialGadget +pub trait FoldingSchemeGadgetOpsFull: + FoldingSchemeGadgetOpsPartial { #[allow(non_snake_case)] fn verify( @@ -396,10 +368,38 @@ pub trait FoldingSchemeFullGadget: transcript: &mut impl TranscriptGadget<::ConstraintField>, Us: [&Self::RU; M], us: [&Self::IU; N], - proof: &Self::Proof, + proof: &Self::Proof, ) -> Result; } +pub trait GroupBasedFoldingSchemePrimary: + FoldingSchemeDef< + VC: GroupBasedCommitment, + TranscriptField = <::VC as CommitmentDef>::Scalar, + > + FoldingSchemeOps +{ + type Gadget: FoldingSchemeGadgetOpsPartial< + M, + N, + Native = Self, + VC = ::Gadget2, + >; +} + +pub trait GroupBasedFoldingSchemeSecondary: + FoldingSchemeDef< + VC: GroupBasedCommitment, + TranscriptField = CF2<<::VC as CommitmentDef>::Commitment>, + > + FoldingSchemeOps +{ + type Gadget: FoldingSchemeGadgetOpsFull< + M, + N, + Native = Self, + VC = ::Gadget1, + >; +} + #[cfg(test)] mod tests { use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; @@ -416,7 +416,7 @@ mod tests { use super::*; #[allow(non_snake_case)] - pub fn test_folding_scheme, const M: usize, const N: usize>( + pub fn test_folding_scheme, const M: usize, const N: usize>( config: FS::Config, circuit: impl ConstraintSynthesizer<::Scalar>, assignments_vec: Vec::Scalar>>, @@ -466,9 +466,9 @@ mod tests { let ws = ws.try_into().unwrap(); let us = us.try_into().unwrap(); - let (WW, UU, pi, _) = FS::prove(&pk, &mut transcript_p, &Ws, &Us, &ws, &us, &mut rng)?; + let (WW, UU, pi, _) = FS::prove(pk, &mut transcript_p, &Ws, &Us, &ws, &us, &mut rng)?; FS::decide_running(&dk, &WW, &UU)?; - assert_eq!(FS::verify(&vk, &mut transcript_v, &Us, &us, &pi)?, UU); + assert_eq!(FS::verify(vk, &mut transcript_v, &Us, &us, &pi)?, UU); for i in 0..M { let (W, U) = WitnessInstanceSampler::::sample(&dk, (), &mut rng)?; diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index 616e9bd8e..a5a5e051d 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -11,7 +11,7 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; use ark_std::marker::PhantomData; use sonobe_fs::{ - FoldingInstanceVar, FoldingSchemeFullGadget, FoldingSchemePartialGadget, + FoldingInstanceVar, FoldingSchemeGadgetOpsFull, FoldingSchemeGadgetOpsPartial, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ @@ -47,7 +47,7 @@ where FS1: FoldingSchemeCycleFoldExt< 1, 1, - Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeGadgetOpsPartial<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -55,7 +55,7 @@ where FS2: GroupBasedFoldingSchemeSecondary< 1, 1, - Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeGadgetOpsFull<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -73,10 +73,10 @@ where external_inputs: FC::ExternalInputs, U: &FS1::RU, u: &FS1::IU, - proof: FS1::Proof, + proof: FS1::Proof<1, 1>, cf_U: &FS2::RU, cf_us: Vec, - cf_proofs: Vec, + cf_proofs: Vec>, ) -> Result<(FC::State, FC::ExternalOutputs), SynthesisError> { let hash = T::Gadget::new_with_pp_hash( &self.hash_config, @@ -163,7 +163,7 @@ where FS1: FoldingSchemeCycleFoldExt< 1, 1, - Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeGadgetOpsPartial<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -171,8 +171,10 @@ where FS2: GroupBasedFoldingSchemeSecondary< 1, 1, - Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef>, + Gadget: FoldingSchemeGadgetOpsFull<1, 1, VerifierKey = ()>, + VC: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, FC: FCircuit::Scalar>, T: Transcript, diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index baaa963ae..197e687cd 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -1,13 +1,14 @@ -use ark_ff::{PrimeField, Zero}; +use ark_ff::Zero; use ark_relations::gr1cs::{ConstraintSystem, SynthesisError, SynthesisMode}; -use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore, sync::Arc}; +use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore}; use sonobe_fs::{ - DeciderKey, FoldingInstance, FoldingScheme, FoldingSchemeFullGadget, - FoldingSchemePartialGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, + DeciderKey, FoldingInstance, FoldingSchemeDef, FoldingSchemeGadgetDef, + FoldingSchemeGadgetOpsFull, FoldingSchemeGadgetOpsPartial, GroupBasedFoldingSchemePrimary, + GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ algebra::field::emulated::EmulatedFieldVar, - arithmetizations::{Arith, ArithConfig}, + arithmetizations::Arith, circuits::{ArithExtractor, AssignmentsExtractor, ConstraintSystemExt, FCircuit}, commitments::{CommitmentDef, CommitmentDefGadget}, relations::WitnessInstanceSampler, @@ -35,16 +36,16 @@ pub trait FoldingSchemeCycleFoldExt: fn to_cyclefold_configs( Us: &[impl Borrow; M], us: &[impl Borrow; N], - proof: &Self::Proof, + proof: &Self::Proof, rho: Self::Challenge, ) -> Vec; fn to_cyclefold_inputs( - Us: [>::RU; M], - us: [>::IU; N], - UU: >::RU, - proof: >::Proof, - rho: >::Challenge, + Us: [::RU; M], + us: [::IU; N], + UU: ::RU, + proof: ::Proof, + rho: ::Challenge, ) -> Result< Vec< Vec< @@ -58,13 +59,13 @@ pub trait FoldingSchemeCycleFoldExt: >; } -pub struct Key, FS2: FoldingScheme<1, 1>, T>( +pub struct Key( pub FS1::DeciderKey, pub FS2::DeciderKey, pub T, ); -pub struct Proof, FS2: FoldingScheme<1, 1>>( +pub struct Proof( pub FS1::RW, pub FS1::RU, pub FS1::IW, @@ -73,9 +74,7 @@ pub struct Proof, FS2: FoldingScheme<1, 1>>( pub FS2::RU, ); -impl, FS2: FoldingScheme<1, 1>, T> Dummy<&Key> - for Proof -{ +impl Dummy<&Key> for Proof { fn dummy(pk: &Key) -> Self { let cfg1 = pk.0.to_arith_config(); let cfg2 = pk.1.to_arith_config(); @@ -100,7 +99,7 @@ where 1, 1, Arith: From::Commitment>>>, - Gadget: FoldingSchemePartialGadget<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeGadgetOpsPartial<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -109,7 +108,7 @@ where 1, 1, Arith: From::Commitment>>>, - Gadget: FoldingSchemeFullGadget<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeGadgetOpsFull<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index 7a660a54e..efe68a390 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -132,10 +132,6 @@ mod tests { use ark_crypto_primitives::sponge::{CryptographicSponge, poseidon::PoseidonSponge}; use ark_grumpkin::Projective as C2; use ark_std::{error::Error, rand::Rng, sync::Arc, test_rng}; - use sonobe_fs::{ - FoldingScheme, FoldingSchemeFullGadget, FoldingSchemePartialGadget, PlainInstance as IU, - PlainInstanceVar as IUVar, PlainWitness as IW, PlainWitnessVar as IWVar, - }; use sonobe_primitives::{ arithmetizations::Arith, circuits::utils::CircuitForTest, From c4ab18e061bf9a949e3eb9e457f5a2f2788a4d96 Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 24 Nov 2025 05:30:03 +0800 Subject: [PATCH 53/93] Improve trait design for group based FS --- crates/fs/src/lib.rs | 60 +++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 0038faf4a..15602b944 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -54,7 +54,9 @@ pub trait FoldingWitness: Debug { fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; } -pub trait FoldingInstance: Clone + Debug + PartialEq + Absorbable { +pub trait FoldingInstance: + Clone + Debug + PartialEq + Eq + Absorbable +{ const N_COMMITMENTS: usize; /// Returns the commitments contained in the committed instance. @@ -372,32 +374,50 @@ pub trait FoldingSchemeGadgetOpsFull: ) -> Result; } +pub trait GroupBasedFoldingSchemePrimaryDef: + FoldingSchemeDef< + VC: GroupBasedCommitment, + TranscriptField = <::VC as CommitmentDef>::Scalar, +> +{ + type Gadget: FoldingSchemeGadgetDef< + Native = Self, + VC = ::Gadget2, + >; +} + pub trait GroupBasedFoldingSchemePrimary: + GroupBasedFoldingSchemePrimaryDef> + + FoldingSchemeOps +{ +} + +impl GroupBasedFoldingSchemePrimary for FS where + FS: GroupBasedFoldingSchemePrimaryDef> +{ +} + +pub trait GroupBasedFoldingSchemeSecondaryDef: FoldingSchemeDef< - VC: GroupBasedCommitment, - TranscriptField = <::VC as CommitmentDef>::Scalar, - > + FoldingSchemeOps + VC: GroupBasedCommitment, + TranscriptField = CF2<<::VC as CommitmentDef>::Commitment>, +> { - type Gadget: FoldingSchemeGadgetOpsPartial< - M, - N, - Native = Self, - VC = ::Gadget2, - >; + type Gadget: FoldingSchemeGadgetDef< + Native = Self, + VC = ::Gadget1, + >; } pub trait GroupBasedFoldingSchemeSecondary: - FoldingSchemeDef< - VC: GroupBasedCommitment, - TranscriptField = CF2<<::VC as CommitmentDef>::Commitment>, - > + FoldingSchemeOps + GroupBasedFoldingSchemeSecondaryDef> + + FoldingSchemeOps +{ +} + +impl GroupBasedFoldingSchemeSecondary for FS where + FS: GroupBasedFoldingSchemeSecondaryDef> { - type Gadget: FoldingSchemeGadgetOpsFull< - M, - N, - Native = Self, - VC = ::Gadget1, - >; } #[cfg(test)] From dbc9fe609fd01a8db4bae3b638e709aa997f4ecc Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 24 Nov 2025 09:34:45 +0800 Subject: [PATCH 54/93] Add trait bounds for in-circuit challenges --- crates/fs/src/lib.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 15602b944..4c2de6590 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -67,7 +67,7 @@ pub trait FoldingInstance: fn public_inputs_mut(&mut self) -> &mut [VC::Scalar]; } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TaggedVec(pub Vec); impl Deref for TaggedVec { @@ -90,6 +90,12 @@ impl From> for TaggedVec { } } +impl From> for Vec { + fn from(val: TaggedVec) -> Self { + val.0 + } +} + impl Absorbable for TaggedVec { fn absorb_into(&self, dest: &mut Vec) { self.0.absorb_into(dest) @@ -338,7 +344,13 @@ pub trait FoldingSchemeGadgetDef { type VerifierKey; - type Challenge; + type Challenge: AllocVar< + ::Challenge, + ::ConstraintField, + > + GR1CSVar< + ::ConstraintField, + Value = ::Challenge, + >; type Proof: AllocVar< ::Proof, ::ConstraintField, From 0fca7de14a3a3a54e9925d3c2eec1afed2783e0b Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 25 Nov 2025 00:52:37 +0800 Subject: [PATCH 55/93] Further split FS Ops --- crates/fs/src/lib.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 4c2de6590..30c298426 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -229,7 +229,7 @@ pub trait FoldingSchemeDef { + for<'a> Dummy<&'a ::Config>; } -pub trait FoldingSchemeOps: FoldingSchemeDef { +pub trait FoldingSchemePreprocessor: FoldingSchemeDef { /// The preprocessing method is a randomized algorithm that takes as input /// the size bounds of the folding scheme, which are contained in the /// `config` parameter, and outputs the public parameters. @@ -239,12 +239,16 @@ pub trait FoldingSchemeOps: FoldingSchemeDef { /// The security parameter is implicitly specified by the size of underlying /// fields and groups. fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; +} +pub trait FoldingSchemeKeyGenerator: FoldingSchemeDef { /// The key generation method is a deterministic algorithm that takes as /// input the public parameters `pp` and the constraint system `arith`, and /// outputs a prover key and a verifier key. fn generate_keys(pp: Self::PublicParam, arith: Self::Arith) -> Result; +} +pub trait FoldingSchemeProver: FoldingSchemeDef { /// The proof generation method is a deterministic algorithm that takes as /// input the prover key `pk`, the transcript `transcript` between the /// prover and the verifier, the first witness-instance pair `W`, `U`, the @@ -264,7 +268,9 @@ pub trait FoldingSchemeOps: FoldingSchemeDef { us: &[impl Borrow; N], rng: impl RngCore, ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; +} +pub trait FoldingSchemeVerifier: FoldingSchemeDef { #[allow(non_snake_case)] fn verify( vk: &::VerifierKey, @@ -273,7 +279,9 @@ pub trait FoldingSchemeOps: FoldingSchemeDef { us: &[impl Borrow; N], proof: &Self::Proof, ) -> Result; +} +pub trait FoldingSchemeDecider: FoldingSchemeDef { #[allow(non_snake_case)] fn decide_running(dk: &Self::DeciderKey, W: &Self::RW, U: &Self::RU) -> Result<(), Error> { Relation::::check_relation(dk, W, U) @@ -284,6 +292,26 @@ pub trait FoldingSchemeOps: FoldingSchemeDef { } } +impl FoldingSchemeDecider for FS {} + +pub trait FoldingSchemeOps: + FoldingSchemePreprocessor + + FoldingSchemeKeyGenerator + + FoldingSchemeProver + + FoldingSchemeVerifier + + FoldingSchemeDecider +{ +} + +impl FoldingSchemeOps for FS where + FS: FoldingSchemePreprocessor + + FoldingSchemeKeyGenerator + + FoldingSchemeProver + + FoldingSchemeVerifier + + FoldingSchemeDecider +{ +} + pub trait FoldingWitnessVar: AllocVar + GR1CSVar> From c7239421e103357de3e2c15bd10cb93af0ff337c Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 26 Dec 2025 22:08:26 +0800 Subject: [PATCH 56/93] Stateful prover no longer owns pk and step circuit --- crates/ivc/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index efe68a390..befd8bf17 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -54,19 +54,19 @@ pub trait IVC { ) -> Result<(), Error>; } -pub struct IVCStatefulProver { - pub pk: I::ProverKey, - pub step_circuit: FC, +pub struct IVCStatefulProver<'a, FC: FCircuit, I: IVC> { + pub pk: &'a I::ProverKey, + pub step_circuit: &'a FC, pub i: usize, pub initial_state: FC::State, pub current_state: FC::State, pub current_proof: I::Proof, } -impl, I: IVC> IVCStatefulProver { +impl<'a, FC: FCircuit, I: IVC> IVCStatefulProver<'a, FC, I> { pub fn new( - pk: I::ProverKey, - step_circuit: FC, + pk: &'a I::ProverKey, + step_circuit: &'a FC, initial_state: FC::State, ) -> Result { Ok(Self { @@ -154,7 +154,7 @@ mod tests { let initial_state = step_circuit.dummy_state(); - let mut prover = IVCStatefulProver::<_, I>::new(pk, step_circuit, initial_state)?; + let mut prover = IVCStatefulProver::<_, I>::new(&pk, &step_circuit, initial_state)?; for external_inputs in external_inputs_vec { prover.prove_step(external_inputs, &mut rng)?; From d0ec53120527d26bfd853fdd711d7d7e3da74f5a Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 57/93] Adjust the naming of traits for gadgets --- crates/fs/src/lib.rs | 22 +++++++++---------- .../ivc/src/compilers/cyclefold/circuits.rs | 10 ++++----- crates/ivc/src/compilers/cyclefold/mod.rs | 18 +++++++-------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 30c298426..9c7cd725d 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -363,7 +363,7 @@ impl FoldingInstanceVar for PlainInstanceVar::VC>; @@ -388,8 +388,8 @@ pub trait FoldingSchemeGadgetDef { >; } -pub trait FoldingSchemeGadgetOpsPartial: - FoldingSchemeGadgetDef> +pub trait FoldingSchemePartialVerifierGadget: + FoldingSchemeDefGadget> { #[allow(non_snake_case)] fn verify_hinted( @@ -401,8 +401,8 @@ pub trait FoldingSchemeGadgetOpsPartial: ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; } -pub trait FoldingSchemeGadgetOpsFull: - FoldingSchemeGadgetOpsPartial +pub trait FoldingSchemeFullVerifierGadget: + FoldingSchemePartialVerifierGadget { #[allow(non_snake_case)] fn verify( @@ -420,20 +420,20 @@ pub trait GroupBasedFoldingSchemePrimaryDef: TranscriptField = <::VC as CommitmentDef>::Scalar, > { - type Gadget: FoldingSchemeGadgetDef< + type Gadget: FoldingSchemeDefGadget< Native = Self, VC = ::Gadget2, >; } pub trait GroupBasedFoldingSchemePrimary: - GroupBasedFoldingSchemePrimaryDef> + GroupBasedFoldingSchemePrimaryDef> + FoldingSchemeOps { } impl GroupBasedFoldingSchemePrimary for FS where - FS: GroupBasedFoldingSchemePrimaryDef> + FS: GroupBasedFoldingSchemePrimaryDef> { } @@ -443,20 +443,20 @@ pub trait GroupBasedFoldingSchemeSecondaryDef: TranscriptField = CF2<<::VC as CommitmentDef>::Commitment>, > { - type Gadget: FoldingSchemeGadgetDef< + type Gadget: FoldingSchemeDefGadget< Native = Self, VC = ::Gadget1, >; } pub trait GroupBasedFoldingSchemeSecondary: - GroupBasedFoldingSchemeSecondaryDef> + GroupBasedFoldingSchemeSecondaryDef> + FoldingSchemeOps { } impl GroupBasedFoldingSchemeSecondary for FS where - FS: GroupBasedFoldingSchemeSecondaryDef> + FS: GroupBasedFoldingSchemeSecondaryDef> { } diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index a5a5e051d..734943c16 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -11,7 +11,7 @@ use ark_r1cs_std::{ use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; use ark_std::marker::PhantomData; use sonobe_fs::{ - FoldingInstanceVar, FoldingSchemeGadgetOpsFull, FoldingSchemeGadgetOpsPartial, + FoldingInstanceVar, FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ @@ -47,7 +47,7 @@ where FS1: FoldingSchemeCycleFoldExt< 1, 1, - Gadget: FoldingSchemeGadgetOpsPartial<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -55,7 +55,7 @@ where FS2: GroupBasedFoldingSchemeSecondary< 1, 1, - Gadget: FoldingSchemeGadgetOpsFull<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -163,7 +163,7 @@ where FS1: FoldingSchemeCycleFoldExt< 1, 1, - Gadget: FoldingSchemeGadgetOpsPartial<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -171,7 +171,7 @@ where FS2: GroupBasedFoldingSchemeSecondary< 1, 1, - Gadget: FoldingSchemeGadgetOpsFull<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 197e687cd..4af38ead6 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -2,8 +2,8 @@ use ark_ff::Zero; use ark_relations::gr1cs::{ConstraintSystem, SynthesisError, SynthesisMode}; use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore}; use sonobe_fs::{ - DeciderKey, FoldingInstance, FoldingSchemeDef, FoldingSchemeGadgetDef, - FoldingSchemeGadgetOpsFull, FoldingSchemeGadgetOpsPartial, GroupBasedFoldingSchemePrimary, + DeciderKey, FoldingInstance, FoldingSchemeDef, FoldingSchemeDefGadget, + FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ @@ -41,11 +41,11 @@ pub trait FoldingSchemeCycleFoldExt: ) -> Vec; fn to_cyclefold_inputs( - Us: [::RU; M], - us: [::IU; N], - UU: ::RU, - proof: ::Proof, - rho: ::Challenge, + Us: [::RU; M], + us: [::IU; N], + UU: ::RU, + proof: ::Proof, + rho: ::Challenge, ) -> Result< Vec< Vec< @@ -99,7 +99,7 @@ where 1, 1, Arith: From::Commitment>>>, - Gadget: FoldingSchemeGadgetOpsPartial<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, @@ -108,7 +108,7 @@ where 1, 1, Arith: From::Commitment>>>, - Gadget: FoldingSchemeGadgetOpsFull<1, 1, VerifierKey = ()>, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, VC: CommitmentDef< Commitment: SonobeCurve::Scalar>, >, From d715587f7b028d1ba3a4eb81ea2e3c163cd985c2 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 58/93] Move trait definitions to their dedicated files --- crates/fs/src/definitions/algorithms.rs | 87 ++++ crates/fs/src/definitions/circuits.rs | 32 ++ crates/fs/src/definitions/errors.rs | 28 ++ crates/fs/src/definitions/instances.rs | 86 ++++ crates/fs/src/definitions/keys.rs | 11 + crates/fs/src/definitions/mod.rs | 75 ++++ crates/fs/src/definitions/utils.rs | 103 +++++ crates/fs/src/definitions/variants.rs | 55 +++ crates/fs/src/definitions/witnesses.rs | 47 +++ crates/fs/src/lib.rs | 483 +--------------------- crates/ivc/src/compilers/cyclefold/mod.rs | 4 +- 11 files changed, 547 insertions(+), 464 deletions(-) create mode 100644 crates/fs/src/definitions/algorithms.rs create mode 100644 crates/fs/src/definitions/circuits.rs create mode 100644 crates/fs/src/definitions/errors.rs create mode 100644 crates/fs/src/definitions/instances.rs create mode 100644 crates/fs/src/definitions/keys.rs create mode 100644 crates/fs/src/definitions/mod.rs create mode 100644 crates/fs/src/definitions/utils.rs create mode 100644 crates/fs/src/definitions/variants.rs create mode 100644 crates/fs/src/definitions/witnesses.rs diff --git a/crates/fs/src/definitions/algorithms.rs b/crates/fs/src/definitions/algorithms.rs new file mode 100644 index 000000000..23642a6bb --- /dev/null +++ b/crates/fs/src/definitions/algorithms.rs @@ -0,0 +1,87 @@ +use ark_std::{borrow::Borrow, rand::RngCore}; +use sonobe_primitives::{relations::Relation, transcripts::Transcript}; + +use super::{errors::Error, keys::DeciderKey, FoldingSchemeDef}; + +pub trait FoldingSchemePreprocessor: FoldingSchemeDef { + /// The preprocessing method is a randomized algorithm that takes as input + /// the size bounds of the folding scheme, which are contained in the + /// `config` parameter, and outputs the public parameters. + /// + /// Here, the randomness source is controlled by `rng`. + /// + /// The security parameter is implicitly specified by the size of underlying + /// fields and groups. + fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; +} + +pub trait FoldingSchemeKeyGenerator: FoldingSchemeDef { + /// The key generation method is a deterministic algorithm that takes as + /// input the public parameters `pp` and the constraint system `arith`, and + /// outputs a prover key and a verifier key. + fn generate_keys(pp: Self::PublicParam, arith: Self::Arith) -> Result; +} + +pub trait FoldingSchemeProver: FoldingSchemeDef { + /// The proof generation method is a deterministic algorithm that takes as + /// input the prover key `pk`, the transcript `transcript` between the + /// prover and the verifier, the first witness-instance pair `W`, `U`, the + /// second witness-instance pair `w`, `u`, and outputs the folded witness + /// and instance, the proof, and the (intermediate) randomness. + /// + /// Here, the randomness source is controlled by `transcript`. The returned + /// intermediate randomness is useful for the construction of CycleFold + /// circuits in our CycleFold-based folding-to-IVC compiler. + #[allow(non_snake_case)] + fn prove( + pk: &::ProverKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; M], + Us: &[impl Borrow; M], + ws: &[impl Borrow; N], + us: &[impl Borrow; N], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; +} + +pub trait FoldingSchemeVerifier: FoldingSchemeDef { + #[allow(non_snake_case)] + fn verify( + vk: &::VerifierKey, + transcript: &mut impl Transcript, + Us: &[impl Borrow; M], + us: &[impl Borrow; N], + proof: &Self::Proof, + ) -> Result; +} + +pub trait FoldingSchemeDecider: FoldingSchemeDef { + #[allow(non_snake_case)] + fn decide_running(dk: &Self::DeciderKey, W: &Self::RW, U: &Self::RU) -> Result<(), Error> { + Relation::::check_relation(dk, W, U) + } + + fn decide_incoming(dk: &Self::DeciderKey, w: &Self::IW, u: &Self::IU) -> Result<(), Error> { + Relation::::check_relation(dk, w, u) + } +} + +impl FoldingSchemeDecider for FS {} + +pub trait FoldingSchemeOps: + FoldingSchemePreprocessor + + FoldingSchemeKeyGenerator + + FoldingSchemeProver + + FoldingSchemeVerifier + + FoldingSchemeDecider +{ +} + +impl FoldingSchemeOps for FS where + FS: FoldingSchemePreprocessor + + FoldingSchemeKeyGenerator + + FoldingSchemeProver + + FoldingSchemeVerifier + + FoldingSchemeDecider +{ +} diff --git a/crates/fs/src/definitions/circuits.rs b/crates/fs/src/definitions/circuits.rs new file mode 100644 index 000000000..1e17fed77 --- /dev/null +++ b/crates/fs/src/definitions/circuits.rs @@ -0,0 +1,32 @@ +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::TranscriptGadget}; + +use super::{ + algorithms::FoldingSchemeOps, FoldingSchemeDefGadget, +}; + +pub trait FoldingSchemePartialVerifierGadget: + FoldingSchemeDefGadget> +{ + #[allow(non_snake_case)] + fn verify_hinted( + vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget<::ConstraintField>, + Us: [&Self::RU; M], + us: [&Self::IU; N], + proof: &Self::Proof, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; +} + +pub trait FoldingSchemeFullVerifierGadget: + FoldingSchemePartialVerifierGadget +{ + #[allow(non_snake_case)] + fn verify( + vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget<::ConstraintField>, + Us: [&Self::RU; M], + us: [&Self::IU; N], + proof: &Self::Proof, + ) -> Result; +} diff --git a/crates/fs/src/definitions/errors.rs b/crates/fs/src/definitions/errors.rs new file mode 100644 index 000000000..de9edfd35 --- /dev/null +++ b/crates/fs/src/definitions/errors.rs @@ -0,0 +1,28 @@ +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + arithmetizations::Error as ArithError, commitments::Error as CommitmentError, + sumcheck::Error as SumCheckError, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + ArithError(#[from] ArithError), + #[error(transparent)] + CommitmentError(#[from] CommitmentError), + #[error(transparent)] + SynthesisError(#[from] SynthesisError), + #[error(transparent)] + SumCheckError(#[from] SumCheckError), + #[error("Unsupported use case: {0}")] + Unsupported(String), + #[error("Failed to create domain")] + DomainCreationFailure, + #[error("Indivisible by vanishing polynomial")] + IndivisibleByVanishingPoly, + #[error("Unsatisfied relation: {0}")] + UnsatisfiedRelation(String), + #[error("Invalid public parameters: {0}")] + InvalidPublicParameters(String), +} diff --git a/crates/fs/src/definitions/instances.rs b/crates/fs/src/definitions/instances.rs new file mode 100644 index 000000000..e54dc487b --- /dev/null +++ b/crates/fs/src/definitions/instances.rs @@ -0,0 +1,86 @@ +use ark_r1cs_std::{alloc::AllocVar, select::CondSelectGadget, GR1CSVar}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_std::fmt::Debug; +use sonobe_primitives::{ + arithmetizations::ArithConfig, + commitments::{CommitmentDef, CommitmentDefGadget}, + traits::Dummy, + transcripts::{Absorbable, AbsorbableVar}, +}; + +use super::utils::TaggedVec; + +pub trait FoldingInstance: + Clone + Debug + PartialEq + Eq + Absorbable +{ + const N_COMMITMENTS: usize; + + /// Returns the commitments contained in the committed instance. + fn commitments(&self) -> Vec<&VC::Commitment>; + + fn public_inputs(&self) -> &[VC::Scalar]; + + fn public_inputs_mut(&mut self) -> &mut [VC::Scalar]; +} + +pub type PlainInstance = TaggedVec; + +impl Dummy<&A> for PlainInstance { + fn dummy(cfg: &A) -> Self { + vec![V::default(); cfg.n_public_inputs()].into() + } +} + +impl FoldingInstance for PlainInstance { + const N_COMMITMENTS: usize = 0; + + fn commitments(&self) -> Vec<&VC::Commitment> { + vec![] + } + + fn public_inputs(&self) -> &[VC::Scalar] { + self + } + + fn public_inputs_mut(&mut self) -> &mut [VC::Scalar] { + self + } +} + +pub trait FoldingInstanceVar: + AllocVar + + GR1CSVar> + + AbsorbableVar + + CondSelectGadget +{ + /// Returns the commitments contained in the committed instance. + fn commitments(&self) -> Vec<&VC::CommitmentVar>; + + fn public_inputs(&self) -> &Vec; + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result; +} + +impl FoldingInstanceVar for PlainInstanceVar { + fn commitments(&self) -> Vec<&VC::CommitmentVar> { + vec![] + } + + fn public_inputs(&self) -> &Vec { + self + } + + fn new_witness_with_public_inputs( + _cs: impl Into>, + _u: &Self::Value, + x: Vec, + ) -> Result { + Ok(Self(x)) + } +} + +pub type PlainInstanceVar = PlainInstance; diff --git a/crates/fs/src/definitions/keys.rs b/crates/fs/src/definitions/keys.rs new file mode 100644 index 000000000..230395e18 --- /dev/null +++ b/crates/fs/src/definitions/keys.rs @@ -0,0 +1,11 @@ +use sonobe_primitives::arithmetizations::ArithConfig; + +pub trait DeciderKey { + type ProverKey; + type VerifierKey; + type ArithConfig: ArithConfig; + + fn to_pk(&self) -> &Self::ProverKey; + fn to_vk(&self) -> &Self::VerifierKey; + fn to_arith_config(&self) -> &Self::ArithConfig; +} diff --git a/crates/fs/src/definitions/mod.rs b/crates/fs/src/definitions/mod.rs new file mode 100644 index 000000000..d1c13a2f3 --- /dev/null +++ b/crates/fs/src/definitions/mod.rs @@ -0,0 +1,75 @@ +pub mod algorithms; +pub mod circuits; +pub mod errors; +pub mod instances; +pub mod keys; +pub mod utils; +pub mod variants; +pub mod witnesses; + +use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; +use sonobe_primitives::{ + arithmetizations::Arith, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentDefGadget}, + relations::{Relation, WitnessInstanceSampler}, + traits::{Dummy, SonobeField}, +}; + +use self::{ + errors::Error, + instances::{FoldingInstance, FoldingInstanceVar}, + keys::DeciderKey, + witnesses::FoldingWitness, +}; + +pub trait FoldingSchemeDef { + type VC: CommitmentDef; + type RW: FoldingWitness + for<'a> Dummy<&'a ::Config>; + type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; + type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + type TranscriptField: SonobeField; + type Arith: Arith::ArithConfig>; + type Config; + type PublicParam; + type DeciderKey: DeciderKey + + Clone + + Relation + + Relation + + WitnessInstanceSampler + + WitnessInstanceSampler< + Self::IW, + Self::IU, + Source = AssignmentsOwned<::Scalar>, + Error = Error, + >; + type Challenge; + type Proof: Clone + + for<'a> Dummy<&'a ::Config>; +} + +pub trait FoldingSchemeDefGadget { + type Native: FoldingSchemeDef; + + type VC: CommitmentDefGadget::VC>; + type RU: FoldingInstanceVar::RU>; + type IU: FoldingInstanceVar::IU>; + + type VerifierKey; + + type Challenge: AllocVar< + ::Challenge, + ::ConstraintField, + > + GR1CSVar< + ::ConstraintField, + Value = ::Challenge, + >; + type Proof: AllocVar< + ::Proof, + ::ConstraintField, + > + GR1CSVar< + ::ConstraintField, + Value = ::Proof, + >; +} diff --git a/crates/fs/src/definitions/utils.rs b/crates/fs/src/definitions/utils.rs new file mode 100644 index 000000000..8d96e02d9 --- /dev/null +++ b/crates/fs/src/definitions/utils.rs @@ -0,0 +1,103 @@ + +use ark_ff::{Field, PrimeField}; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, + GR1CSVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{ + borrow::Borrow, + ops::{Deref, DerefMut}, +}; +use sonobe_primitives::transcripts::{Absorbable, AbsorbableVar}; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TaggedVec(pub Vec); + +impl Deref for TaggedVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TaggedVec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From> for TaggedVec { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl From> for Vec { + fn from(val: TaggedVec) -> Self { + val.0 + } +} + +impl Absorbable for TaggedVec { + fn absorb_into(&self, dest: &mut Vec) { + self.0.absorb_into(dest) + } +} + +impl, const TAG: char> AbsorbableVar + for TaggedVec +{ + fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { + self.0.absorb_into(dest) + } +} + +impl, Y, const TAG: char> AllocVar, F> + for TaggedVec +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let v = f()?; + Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(Self) + } +} + +impl, const TAG: char> CondSelectGadget + for TaggedVec +{ + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.len() != false_value.len() { + return Err(SynthesisError::Unsatisfiable); + } + true_value + .iter() + .zip(false_value.iter()) + .map(|(t, f)| cond.select(t, f)) + .collect::>() + .map(Self) + } +} + +impl, const TAG: char> GR1CSVar for TaggedVec { + type Value = TaggedVec; + + fn cs(&self) -> ConstraintSystemRef { + self.0.cs() + } + + fn value(&self) -> Result { + self.0.value().map(TaggedVec) + } +} diff --git a/crates/fs/src/definitions/variants.rs b/crates/fs/src/definitions/variants.rs new file mode 100644 index 000000000..86ab90581 --- /dev/null +++ b/crates/fs/src/definitions/variants.rs @@ -0,0 +1,55 @@ +use sonobe_primitives::{ + commitments::{GroupBasedCommitment, CommitmentDef}, + traits::CF2, +}; + +use crate::{ + FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, FoldingSchemeOps, + FoldingSchemePartialVerifierGadget, +}; + +pub trait GroupBasedFoldingSchemePrimaryDef: + FoldingSchemeDef< + VC: GroupBasedCommitment, + TranscriptField = <::VC as CommitmentDef>::Scalar, +> +{ + type Gadget: FoldingSchemeDefGadget< + Native = Self, + VC = ::Gadget2, + >; +} + +pub trait GroupBasedFoldingSchemePrimary: + GroupBasedFoldingSchemePrimaryDef> + + FoldingSchemeOps +{ +} + +impl GroupBasedFoldingSchemePrimary for FS where + FS: GroupBasedFoldingSchemePrimaryDef> +{ +} + +pub trait GroupBasedFoldingSchemeSecondaryDef: + FoldingSchemeDef< + VC: GroupBasedCommitment, + TranscriptField = CF2<<::VC as CommitmentDef>::Commitment>, +> +{ + type Gadget: FoldingSchemeDefGadget< + Native = Self, + VC = ::Gadget1, + >; +} + +pub trait GroupBasedFoldingSchemeSecondary: + GroupBasedFoldingSchemeSecondaryDef> + + FoldingSchemeOps +{ +} + +impl GroupBasedFoldingSchemeSecondary for FS where + FS: GroupBasedFoldingSchemeSecondaryDef> +{ +} diff --git a/crates/fs/src/definitions/witnesses.rs b/crates/fs/src/definitions/witnesses.rs new file mode 100644 index 000000000..c20833e5c --- /dev/null +++ b/crates/fs/src/definitions/witnesses.rs @@ -0,0 +1,47 @@ +use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; +use ark_std::fmt::Debug; +use sonobe_primitives::{ + arithmetizations::ArithConfig, + commitments::{CommitmentDef, CommitmentDefGadget}, + traits::Dummy, +}; + +use super::utils::TaggedVec; + +pub trait FoldingWitness: Debug { + const N_OPENINGS: usize; + + /// Returns the reference to all openings contained in the witness, each + /// being a tuple of the values being committed to and the randomness. + fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; +} + +pub type PlainWitness = TaggedVec; + +impl Dummy<&A> for PlainWitness { + fn dummy(cfg: &A) -> Self { + vec![V::default(); cfg.n_witnesses()].into() + } +} + +impl FoldingWitness for PlainWitness { + const N_OPENINGS: usize = 0; + + fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { + vec![] + } +} + +pub trait FoldingWitnessVar: + AllocVar + + GR1CSVar> +{ +} + +impl FoldingWitnessVar for T where + T: AllocVar + + GR1CSVar> +{ +} + +pub type PlainWitnessVar = PlainWitness; diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 9c7cd725d..c4716e408 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -1,475 +1,34 @@ -use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::{ - GR1CSVar, - alloc::{AllocVar, AllocationMode}, - fields::fp::FpVar, - prelude::Boolean, - select::CondSelectGadget, +pub mod definitions; + +pub use self::definitions::{ + FoldingSchemeDef, FoldingSchemeDefGadget, + algorithms::{ + FoldingSchemeDecider, FoldingSchemeKeyGenerator, FoldingSchemeOps, + FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, + }, + circuits::{FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget}, + errors::Error, + instances::{FoldingInstance, FoldingInstanceVar, PlainInstance, PlainInstanceVar}, + keys::DeciderKey, + utils::TaggedVec, + variants::{ + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemePrimaryDef, + GroupBasedFoldingSchemeSecondary, GroupBasedFoldingSchemeSecondaryDef, + }, + witnesses::{FoldingWitness, FoldingWitnessVar, PlainWitness, PlainWitnessVar}, }; -use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{ - borrow::Borrow, - fmt::Debug, - ops::{Deref, DerefMut}, - rand::RngCore, -}; -use sonobe_primitives::{ - arithmetizations::{Arith, ArithConfig}, - circuits::AssignmentsOwned, - commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, - relations::{Relation, WitnessInstanceSampler}, - sumcheck::Error as SumCheckError, - traits::{CF2, Dummy, SonobeField}, - transcripts::{Absorbable, AbsorbableVar, Transcript, TranscriptGadget}, -}; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum Error { - #[error(transparent)] - ArithError(#[from] sonobe_primitives::arithmetizations::Error), - #[error(transparent)] - CommitmentError(#[from] sonobe_primitives::commitments::Error), - #[error(transparent)] - SynthesisError(#[from] SynthesisError), - #[error(transparent)] - SumCheckError(#[from] SumCheckError), - #[error("Unsupported use case: {0}")] - Unsupported(String), - #[error("Failed to create domain")] - DomainCreationFailure, - #[error("Indivisible by vanishing polynomial")] - IndivisibleByVanishingPoly, - #[error("Unsatisfied relation: {0}")] - UnsatisfiedRelation(String), - #[error("Invalid public parameters: {0}")] - InvalidPublicParameters(String), -} - -pub trait FoldingWitness: Debug { - const N_OPENINGS: usize; - - /// Returns the reference to all openings contained in the witness, each - /// being a tuple of the values being committed to and the randomness. - fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; -} - -pub trait FoldingInstance: - Clone + Debug + PartialEq + Eq + Absorbable -{ - const N_COMMITMENTS: usize; - - /// Returns the commitments contained in the committed instance. - fn commitments(&self) -> Vec<&VC::Commitment>; - - fn public_inputs(&self) -> &[VC::Scalar]; - - fn public_inputs_mut(&mut self) -> &mut [VC::Scalar]; -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TaggedVec(pub Vec); - -impl Deref for TaggedVec { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for TaggedVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From> for TaggedVec { - fn from(v: Vec) -> Self { - Self(v) - } -} - -impl From> for Vec { - fn from(val: TaggedVec) -> Self { - val.0 - } -} - -impl Absorbable for TaggedVec { - fn absorb_into(&self, dest: &mut Vec) { - self.0.absorb_into(dest) - } -} - -impl, const TAG: char> AbsorbableVar for TaggedVec { - fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { - self.0.absorb_into(dest) - } -} - -impl, Y, const TAG: char> AllocVar, F> - for TaggedVec -{ - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - let v = f()?; - Vec::new_variable(cs, || Ok(&v.borrow()[..]), mode).map(Self) - } -} - -impl, const TAG: char> CondSelectGadget - for TaggedVec -{ - fn conditionally_select( - cond: &Boolean, - true_value: &Self, - false_value: &Self, - ) -> Result { - if true_value.len() != false_value.len() { - return Err(SynthesisError::Unsatisfiable); - } - true_value - .iter() - .zip(false_value.iter()) - .map(|(t, f)| cond.select(t, f)) - .collect::>() - .map(Self) - } -} - -impl, const TAG: char> GR1CSVar for TaggedVec { - type Value = TaggedVec; - - fn cs(&self) -> ConstraintSystemRef { - self.0.cs() - } - - fn value(&self) -> Result { - self.0.value().map(TaggedVec) - } -} - -pub type PlainWitness = TaggedVec; - -impl Dummy<&A> for PlainWitness { - fn dummy(cfg: &A) -> Self { - vec![V::default(); cfg.n_witnesses()].into() - } -} - -impl FoldingWitness for PlainWitness { - const N_OPENINGS: usize = 0; - - fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { - vec![] - } -} - -pub type PlainInstance = TaggedVec; - -impl Dummy<&A> for PlainInstance { - fn dummy(cfg: &A) -> Self { - vec![V::default(); cfg.n_public_inputs()].into() - } -} - -impl FoldingInstance for PlainInstance { - const N_COMMITMENTS: usize = 0; - - fn commitments(&self) -> Vec<&VC::Commitment> { - vec![] - } - - fn public_inputs(&self) -> &[VC::Scalar] { - self - } - - fn public_inputs_mut(&mut self) -> &mut [VC::Scalar] { - self - } -} - -pub trait DeciderKey { - type ProverKey; - type VerifierKey; - type ArithConfig: ArithConfig; - - fn to_pk(&self) -> &Self::ProverKey; - fn to_vk(&self) -> &Self::VerifierKey; - fn to_arith_config(&self) -> &Self::ArithConfig; -} - -pub trait FoldingSchemeDef { - type VC: CommitmentDef; - type RW: FoldingWitness + for<'a> Dummy<&'a ::Config>; - type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; - type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; - type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; - type TranscriptField: SonobeField; - type Arith: Arith::ArithConfig>; - type Config; - type PublicParam; - type DeciderKey: DeciderKey - + Clone - + Relation - + Relation - + WitnessInstanceSampler - + WitnessInstanceSampler< - Self::IW, - Self::IU, - Source = AssignmentsOwned<::Scalar>, - Error = Error, - >; - type Challenge; - type Proof: Clone - + for<'a> Dummy<&'a ::Config>; -} - -pub trait FoldingSchemePreprocessor: FoldingSchemeDef { - /// The preprocessing method is a randomized algorithm that takes as input - /// the size bounds of the folding scheme, which are contained in the - /// `config` parameter, and outputs the public parameters. - /// - /// Here, the randomness source is controlled by `rng`. - /// - /// The security parameter is implicitly specified by the size of underlying - /// fields and groups. - fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; -} - -pub trait FoldingSchemeKeyGenerator: FoldingSchemeDef { - /// The key generation method is a deterministic algorithm that takes as - /// input the public parameters `pp` and the constraint system `arith`, and - /// outputs a prover key and a verifier key. - fn generate_keys(pp: Self::PublicParam, arith: Self::Arith) -> Result; -} - -pub trait FoldingSchemeProver: FoldingSchemeDef { - /// The proof generation method is a deterministic algorithm that takes as - /// input the prover key `pk`, the transcript `transcript` between the - /// prover and the verifier, the first witness-instance pair `W`, `U`, the - /// second witness-instance pair `w`, `u`, and outputs the folded witness - /// and instance, the proof, and the (intermediate) randomness. - /// - /// Here, the randomness source is controlled by `transcript`. The returned - /// intermediate randomness is useful for the construction of CycleFold - /// circuits in our CycleFold-based folding-to-IVC compiler. - #[allow(non_snake_case)] - fn prove( - pk: &::ProverKey, - transcript: &mut impl Transcript, - Ws: &[impl Borrow; M], - Us: &[impl Borrow; M], - ws: &[impl Borrow; N], - us: &[impl Borrow; N], - rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; -} - -pub trait FoldingSchemeVerifier: FoldingSchemeDef { - #[allow(non_snake_case)] - fn verify( - vk: &::VerifierKey, - transcript: &mut impl Transcript, - Us: &[impl Borrow; M], - us: &[impl Borrow; N], - proof: &Self::Proof, - ) -> Result; -} - -pub trait FoldingSchemeDecider: FoldingSchemeDef { - #[allow(non_snake_case)] - fn decide_running(dk: &Self::DeciderKey, W: &Self::RW, U: &Self::RU) -> Result<(), Error> { - Relation::::check_relation(dk, W, U) - } - - fn decide_incoming(dk: &Self::DeciderKey, w: &Self::IW, u: &Self::IU) -> Result<(), Error> { - Relation::::check_relation(dk, w, u) - } -} - -impl FoldingSchemeDecider for FS {} - -pub trait FoldingSchemeOps: - FoldingSchemePreprocessor - + FoldingSchemeKeyGenerator - + FoldingSchemeProver - + FoldingSchemeVerifier - + FoldingSchemeDecider -{ -} - -impl FoldingSchemeOps for FS where - FS: FoldingSchemePreprocessor - + FoldingSchemeKeyGenerator - + FoldingSchemeProver - + FoldingSchemeVerifier - + FoldingSchemeDecider -{ -} - -pub trait FoldingWitnessVar: - AllocVar - + GR1CSVar> -{ -} - -impl FoldingWitnessVar for T where - T: AllocVar - + GR1CSVar> -{ -} - -pub trait FoldingInstanceVar: - AllocVar - + GR1CSVar> - + AbsorbableVar - + CondSelectGadget -{ - /// Returns the commitments contained in the committed instance. - fn commitments(&self) -> Vec<&VC::CommitmentVar>; - - fn public_inputs(&self) -> &Vec; - - fn new_witness_with_public_inputs( - cs: impl Into>, - u: &Self::Value, - x: Vec, - ) -> Result; -} - -pub type PlainWitnessVar = PlainWitness; -pub type PlainInstanceVar = PlainInstance; - -impl FoldingInstanceVar for PlainInstanceVar { - fn commitments(&self) -> Vec<&VC::CommitmentVar> { - vec![] - } - - fn public_inputs(&self) -> &Vec { - self - } - - fn new_witness_with_public_inputs( - _cs: impl Into>, - _u: &Self::Value, - x: Vec, - ) -> Result { - Ok(Self(x)) - } -} - -pub trait FoldingSchemeDefGadget { - type Native: FoldingSchemeDef; - - type VC: CommitmentDefGadget::VC>; - type RU: FoldingInstanceVar::RU>; - type IU: FoldingInstanceVar::IU>; - - type VerifierKey; - - type Challenge: AllocVar< - ::Challenge, - ::ConstraintField, - > + GR1CSVar< - ::ConstraintField, - Value = ::Challenge, - >; - type Proof: AllocVar< - ::Proof, - ::ConstraintField, - > + GR1CSVar< - ::ConstraintField, - Value = ::Proof, - >; -} - -pub trait FoldingSchemePartialVerifierGadget: - FoldingSchemeDefGadget> -{ - #[allow(non_snake_case)] - fn verify_hinted( - vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget<::ConstraintField>, - Us: [&Self::RU; M], - us: [&Self::IU; N], - proof: &Self::Proof, - ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; -} - -pub trait FoldingSchemeFullVerifierGadget: - FoldingSchemePartialVerifierGadget -{ - #[allow(non_snake_case)] - fn verify( - vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget<::ConstraintField>, - Us: [&Self::RU; M], - us: [&Self::IU; N], - proof: &Self::Proof, - ) -> Result; -} - -pub trait GroupBasedFoldingSchemePrimaryDef: - FoldingSchemeDef< - VC: GroupBasedCommitment, - TranscriptField = <::VC as CommitmentDef>::Scalar, -> -{ - type Gadget: FoldingSchemeDefGadget< - Native = Self, - VC = ::Gadget2, - >; -} - -pub trait GroupBasedFoldingSchemePrimary: - GroupBasedFoldingSchemePrimaryDef> - + FoldingSchemeOps -{ -} - -impl GroupBasedFoldingSchemePrimary for FS where - FS: GroupBasedFoldingSchemePrimaryDef> -{ -} - -pub trait GroupBasedFoldingSchemeSecondaryDef: - FoldingSchemeDef< - VC: GroupBasedCommitment, - TranscriptField = CF2<<::VC as CommitmentDef>::Commitment>, -> -{ - type Gadget: FoldingSchemeDefGadget< - Native = Self, - VC = ::Gadget1, - >; -} - -pub trait GroupBasedFoldingSchemeSecondary: - GroupBasedFoldingSchemeSecondaryDef> - + FoldingSchemeOps -{ -} - -impl GroupBasedFoldingSchemeSecondary for FS where - FS: GroupBasedFoldingSchemeSecondaryDef> -{ -} #[cfg(test)] mod tests { - use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; use ark_std::{error::Error, rand::Rng, sync::Arc}; use sonobe_primitives::{ circuits::{ArithExtractor, AssignmentsOwned}, + commitments::CommitmentDef, + relations::WitnessInstanceSampler, transcripts::{ + Transcript, griffin::{GriffinParams, sponge::GriffinSponge}, - poseidon::poseidon_canonical_config, }, }; diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 4af38ead6..aff040c1f 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -3,8 +3,8 @@ use ark_relations::gr1cs::{ConstraintSystem, SynthesisError, SynthesisMode}; use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore}; use sonobe_fs::{ DeciderKey, FoldingInstance, FoldingSchemeDef, FoldingSchemeDefGadget, - FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, GroupBasedFoldingSchemePrimary, - GroupBasedFoldingSchemeSecondary, + FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, + GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ algebra::field::emulated::EmulatedFieldVar, From 727ba2d7ecf521e46cfe027e80df131be14f4eea Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 59/93] Clean up --- crates/ivc/src/compilers/cyclefold/circuits.rs | 1 + crates/ivc/src/compilers/cyclefold/mod.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index 734943c16..a9f9ca9b3 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -63,6 +63,7 @@ where FC: FCircuit::Scalar>, T: Transcript, { + #[allow(non_snake_case)] pub fn compute_next_state( &self, cs: ConstraintSystemRef, diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index aff040c1f..4494ed24e 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -33,6 +33,7 @@ pub trait FoldingSchemeCycleFoldExt: const N_CYCLEFOLDS: usize; + #[allow(non_snake_case)] fn to_cyclefold_configs( Us: &[impl Borrow; M], us: &[impl Borrow; N], @@ -40,6 +41,7 @@ pub trait FoldingSchemeCycleFoldExt: rho: Self::Challenge, ) -> Vec; + #[allow(non_snake_case)] fn to_cyclefold_inputs( Us: [::RU; M], us: [::IU; N], @@ -178,6 +180,7 @@ where )) } + #[allow(non_snake_case)] fn prove>( Key(dk1, dk2, (hash_config, pp_hash)): &Self::ProverKey, step_circuit: &FC, @@ -267,6 +270,7 @@ where )) } + #[allow(non_snake_case)] fn verify>( Key(dk1, dk2, (hash_config, pp_hash)): &Self::VerifierKey, i: usize, From 461c0ef66411e9eab79185db9be92e90b232a9c2 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 15:50:26 +0800 Subject: [PATCH 60/93] Improve naming and API design --- crates/fs/src/definitions/algorithms.rs | 2 +- crates/fs/src/definitions/circuits.rs | 8 +-- crates/fs/src/definitions/instances.rs | 48 +++++++------ crates/fs/src/definitions/mod.rs | 28 ++++---- crates/fs/src/definitions/utils.rs | 3 +- crates/fs/src/definitions/variants.rs | 24 +++---- crates/fs/src/definitions/witnesses.rs | 22 +++--- crates/fs/src/lib.rs | 6 +- .../ivc/src/compilers/cyclefold/circuits.rs | 68 +++++++++---------- crates/ivc/src/compilers/cyclefold/mod.rs | 44 ++++++------ crates/ivc/src/compilers/mod.rs | 1 - 11 files changed, 116 insertions(+), 138 deletions(-) diff --git a/crates/fs/src/definitions/algorithms.rs b/crates/fs/src/definitions/algorithms.rs index 23642a6bb..607c83d64 100644 --- a/crates/fs/src/definitions/algorithms.rs +++ b/crates/fs/src/definitions/algorithms.rs @@ -1,7 +1,7 @@ use ark_std::{borrow::Borrow, rand::RngCore}; use sonobe_primitives::{relations::Relation, transcripts::Transcript}; -use super::{errors::Error, keys::DeciderKey, FoldingSchemeDef}; +use super::{FoldingSchemeDef, errors::Error, keys::DeciderKey}; pub trait FoldingSchemePreprocessor: FoldingSchemeDef { /// The preprocessing method is a randomized algorithm that takes as input diff --git a/crates/fs/src/definitions/circuits.rs b/crates/fs/src/definitions/circuits.rs index 1e17fed77..5c127dcd6 100644 --- a/crates/fs/src/definitions/circuits.rs +++ b/crates/fs/src/definitions/circuits.rs @@ -1,9 +1,7 @@ use ark_relations::gr1cs::SynthesisError; use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::TranscriptGadget}; -use super::{ - algorithms::FoldingSchemeOps, FoldingSchemeDefGadget, -}; +use super::{FoldingSchemeDefGadget, algorithms::FoldingSchemeOps}; pub trait FoldingSchemePartialVerifierGadget: FoldingSchemeDefGadget> @@ -11,7 +9,7 @@ pub trait FoldingSchemePartialVerifierGadget: #[allow(non_snake_case)] fn verify_hinted( vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget<::ConstraintField>, + transcript: &mut impl TranscriptGadget<::ConstraintField>, Us: [&Self::RU; M], us: [&Self::IU; N], proof: &Self::Proof, @@ -24,7 +22,7 @@ pub trait FoldingSchemeFullVerifierGadget: #[allow(non_snake_case)] fn verify( vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget<::ConstraintField>, + transcript: &mut impl TranscriptGadget<::ConstraintField>, Us: [&Self::RU; M], us: [&Self::IU; N], proof: &Self::Proof, diff --git a/crates/fs/src/definitions/instances.rs b/crates/fs/src/definitions/instances.rs index e54dc487b..f24a44c60 100644 --- a/crates/fs/src/definitions/instances.rs +++ b/crates/fs/src/definitions/instances.rs @@ -1,4 +1,4 @@ -use ark_r1cs_std::{alloc::AllocVar, select::CondSelectGadget, GR1CSVar}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, select::CondSelectGadget}; use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::fmt::Debug; use sonobe_primitives::{ @@ -10,17 +10,15 @@ use sonobe_primitives::{ use super::utils::TaggedVec; -pub trait FoldingInstance: - Clone + Debug + PartialEq + Eq + Absorbable -{ +pub trait FoldingInstance: Clone + Debug + PartialEq + Eq + Absorbable { const N_COMMITMENTS: usize; /// Returns the commitments contained in the committed instance. - fn commitments(&self) -> Vec<&VC::Commitment>; + fn commitments(&self) -> Vec<&CM::Commitment>; - fn public_inputs(&self) -> &[VC::Scalar]; + fn public_inputs(&self) -> &[CM::Scalar]; - fn public_inputs_mut(&mut self) -> &mut [VC::Scalar]; + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar]; } pub type PlainInstance = TaggedVec; @@ -31,53 +29,53 @@ impl Dummy<&A> for PlainInstance { } } -impl FoldingInstance for PlainInstance { +impl FoldingInstance for PlainInstance { const N_COMMITMENTS: usize = 0; - fn commitments(&self) -> Vec<&VC::Commitment> { + fn commitments(&self) -> Vec<&CM::Commitment> { vec![] } - fn public_inputs(&self) -> &[VC::Scalar] { + fn public_inputs(&self) -> &[CM::Scalar] { self } - fn public_inputs_mut(&mut self) -> &mut [VC::Scalar] { + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { self } } -pub trait FoldingInstanceVar: - AllocVar - + GR1CSVar> - + AbsorbableVar - + CondSelectGadget +pub trait FoldingInstanceVar: + AllocVar + + GR1CSVar> + + AbsorbableVar + + CondSelectGadget { /// Returns the commitments contained in the committed instance. - fn commitments(&self) -> Vec<&VC::CommitmentVar>; + fn commitments(&self) -> Vec<&CM::CommitmentVar>; - fn public_inputs(&self) -> &Vec; + fn public_inputs(&self) -> &Vec; fn new_witness_with_public_inputs( - cs: impl Into>, + cs: impl Into>, u: &Self::Value, - x: Vec, + x: Vec, ) -> Result; } -impl FoldingInstanceVar for PlainInstanceVar { - fn commitments(&self) -> Vec<&VC::CommitmentVar> { +impl FoldingInstanceVar for PlainInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { vec![] } - fn public_inputs(&self) -> &Vec { + fn public_inputs(&self) -> &Vec { self } fn new_witness_with_public_inputs( - _cs: impl Into>, + _cs: impl Into>, _u: &Self::Value, - x: Vec, + x: Vec, ) -> Result { Ok(Self(x)) } diff --git a/crates/fs/src/definitions/mod.rs b/crates/fs/src/definitions/mod.rs index d1c13a2f3..77d45952e 100644 --- a/crates/fs/src/definitions/mod.rs +++ b/crates/fs/src/definitions/mod.rs @@ -7,7 +7,7 @@ pub mod utils; pub mod variants; pub mod witnesses; -use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; use sonobe_primitives::{ arithmetizations::Arith, circuits::AssignmentsOwned, @@ -24,11 +24,11 @@ use self::{ }; pub trait FoldingSchemeDef { - type VC: CommitmentDef; - type RW: FoldingWitness + for<'a> Dummy<&'a ::Config>; - type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; - type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; - type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + type CM: CommitmentDef; + type RW: FoldingWitness + for<'a> Dummy<&'a ::Config>; + type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; + type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; type TranscriptField: SonobeField; type Arith: Arith::ArithConfig>; type Config; @@ -41,7 +41,7 @@ pub trait FoldingSchemeDef { + WitnessInstanceSampler< Self::IW, Self::IU, - Source = AssignmentsOwned<::Scalar>, + Source = AssignmentsOwned<::Scalar>, Error = Error, >; type Challenge; @@ -52,24 +52,24 @@ pub trait FoldingSchemeDef { pub trait FoldingSchemeDefGadget { type Native: FoldingSchemeDef; - type VC: CommitmentDefGadget::VC>; - type RU: FoldingInstanceVar::RU>; - type IU: FoldingInstanceVar::IU>; + type CM: CommitmentDefGadget::CM>; + type RU: FoldingInstanceVar::RU>; + type IU: FoldingInstanceVar::IU>; type VerifierKey; type Challenge: AllocVar< ::Challenge, - ::ConstraintField, + ::ConstraintField, > + GR1CSVar< - ::ConstraintField, + ::ConstraintField, Value = ::Challenge, >; type Proof: AllocVar< ::Proof, - ::ConstraintField, + ::ConstraintField, > + GR1CSVar< - ::ConstraintField, + ::ConstraintField, Value = ::Proof, >; } diff --git a/crates/fs/src/definitions/utils.rs b/crates/fs/src/definitions/utils.rs index 8d96e02d9..51e2dd49b 100644 --- a/crates/fs/src/definitions/utils.rs +++ b/crates/fs/src/definitions/utils.rs @@ -1,11 +1,10 @@ - use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ + GR1CSVar, alloc::{AllocVar, AllocationMode}, fields::fp::FpVar, prelude::Boolean, select::CondSelectGadget, - GR1CSVar, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{ diff --git a/crates/fs/src/definitions/variants.rs b/crates/fs/src/definitions/variants.rs index 86ab90581..d8bcae599 100644 --- a/crates/fs/src/definitions/variants.rs +++ b/crates/fs/src/definitions/variants.rs @@ -1,5 +1,5 @@ use sonobe_primitives::{ - commitments::{GroupBasedCommitment, CommitmentDef}, + commitments::{CommitmentDef, GroupBasedCommitment}, traits::CF2, }; @@ -10,14 +10,11 @@ use crate::{ pub trait GroupBasedFoldingSchemePrimaryDef: FoldingSchemeDef< - VC: GroupBasedCommitment, - TranscriptField = <::VC as CommitmentDef>::Scalar, -> + CM: GroupBasedCommitment, + TranscriptField = <::CM as CommitmentDef>::Scalar, + > { - type Gadget: FoldingSchemeDefGadget< - Native = Self, - VC = ::Gadget2, - >; + type Gadget: FoldingSchemeDefGadget::Gadget2>; } pub trait GroupBasedFoldingSchemePrimary: @@ -33,14 +30,11 @@ impl GroupBasedFoldingSchemePrimary fo pub trait GroupBasedFoldingSchemeSecondaryDef: FoldingSchemeDef< - VC: GroupBasedCommitment, - TranscriptField = CF2<<::VC as CommitmentDef>::Commitment>, -> + CM: GroupBasedCommitment, + TranscriptField = CF2<<::CM as CommitmentDef>::Commitment>, + > { - type Gadget: FoldingSchemeDefGadget< - Native = Self, - VC = ::Gadget1, - >; + type Gadget: FoldingSchemeDefGadget::Gadget1>; } pub trait GroupBasedFoldingSchemeSecondary: diff --git a/crates/fs/src/definitions/witnesses.rs b/crates/fs/src/definitions/witnesses.rs index c20833e5c..783bd3f1f 100644 --- a/crates/fs/src/definitions/witnesses.rs +++ b/crates/fs/src/definitions/witnesses.rs @@ -1,4 +1,4 @@ -use ark_r1cs_std::{alloc::AllocVar, GR1CSVar}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; use ark_std::fmt::Debug; use sonobe_primitives::{ arithmetizations::ArithConfig, @@ -8,12 +8,12 @@ use sonobe_primitives::{ use super::utils::TaggedVec; -pub trait FoldingWitness: Debug { +pub trait FoldingWitness: Debug { const N_OPENINGS: usize; /// Returns the reference to all openings contained in the witness, each /// being a tuple of the values being committed to and the randomness. - fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)>; + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)>; } pub type PlainWitness = TaggedVec; @@ -24,23 +24,23 @@ impl Dummy<&A> for PlainWitness { } } -impl FoldingWitness for PlainWitness { +impl FoldingWitness for PlainWitness { const N_OPENINGS: usize = 0; - fn openings(&self) -> Vec<(&[VC::Scalar], &VC::Randomness)> { + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { vec![] } } -pub trait FoldingWitnessVar: - AllocVar - + GR1CSVar> +pub trait FoldingWitnessVar: + AllocVar + + GR1CSVar> { } -impl FoldingWitnessVar for T where - T: AllocVar - + GR1CSVar> +impl FoldingWitnessVar for T where + T: AllocVar + + GR1CSVar> { } diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index c4716e408..0d92bed27 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -37,12 +37,12 @@ mod tests { #[allow(non_snake_case)] pub fn test_folding_scheme, const M: usize, const N: usize>( config: FS::Config, - circuit: impl ConstraintSynthesizer<::Scalar>, - assignments_vec: Vec::Scalar>>, + circuit: impl ConstraintSynthesizer<::Scalar>, + assignments_vec: Vec::Scalar>>, mut rng: impl Rng, ) -> Result<(), Box> where - FS::Arith: From::Scalar>>, + FS::Arith: From::Scalar>>, { let pp = FS::preprocess(config, &mut rng)?; diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index a9f9ca9b3..b0bdbcb79 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -2,11 +2,9 @@ use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, - boolean::Boolean, convert::ToConstraintFieldGadget, eq::EqGadget, fields::{FieldVar, fp::FpVar}, - groups::CurveVar, }; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; use ark_std::marker::PhantomData; @@ -17,14 +15,10 @@ use sonobe_fs::{ use sonobe_primitives::{ algebra::Val, arithmetizations::Arith, - circuits::{ConstraintSystemExt, FCircuit}, - commitments::{CommitmentDef, CommitmentDefGadget}, - relations::WitnessInstanceSampler, - traits::{CF1, CF2, Dummy, SonobeCurve, SonobeField}, - transcripts::{ - Transcript, TranscriptGadget, - griffin::{GriffinParams, sponge::GriffinSpongeVar}, - }, + circuits::FCircuit, + commitments::CommitmentDef, + traits::{CF2, Dummy, SonobeCurve}, + transcripts::{Transcript, TranscriptGadget}, }; use crate::compilers::cyclefold::FoldingSchemeCycleFoldExt; @@ -45,22 +39,22 @@ pub struct AugmentedCircuit< impl<'a, FS1, FS2, FC, T> AugmentedCircuit<'a, FS1, FS2, FC, T> where FS1: FoldingSchemeCycleFoldExt< - 1, - 1, - Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + 1, + 1, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, FS2: GroupBasedFoldingSchemeSecondary< - 1, - 1, - Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + 1, + 1, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, - FC: FCircuit::Scalar>, + FC: FCircuit::Scalar>, T: Transcript, { #[allow(non_snake_case)] @@ -162,22 +156,22 @@ where impl<'a, FS1, FS2, FC, T> ConstraintSynthesizer for AugmentedCircuit<'a, FS1, FS2, FC, T> where FS1: FoldingSchemeCycleFoldExt< - 1, - 1, - Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + 1, + 1, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, FS2: GroupBasedFoldingSchemeSecondary< - 1, - 1, - Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + 1, + 1, + Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, - FC: FCircuit::Scalar>, + FC: FCircuit::Scalar>, T: Transcript, { fn generate_constraints( @@ -233,7 +227,7 @@ pub trait CycleFoldConfig: Sized + Default { } fn verify_point_rlc(&self, cs: ConstraintSystemRef>) - -> Result<(), SynthesisError>; + -> Result<(), SynthesisError>; } #[derive(Debug, Clone, Default)] diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 4494ed24e..fcfe007a8 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -1,5 +1,5 @@ use ark_ff::Zero; -use ark_relations::gr1cs::{ConstraintSystem, SynthesisError, SynthesisMode}; +use ark_relations::gr1cs::{ConstraintSystem, SynthesisError}; use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore}; use sonobe_fs::{ DeciderKey, FoldingInstance, FoldingSchemeDef, FoldingSchemeDefGadget, @@ -9,14 +9,11 @@ use sonobe_fs::{ use sonobe_primitives::{ algebra::field::emulated::EmulatedFieldVar, arithmetizations::Arith, - circuits::{ArithExtractor, AssignmentsExtractor, ConstraintSystemExt, FCircuit}, - commitments::{CommitmentDef, CommitmentDefGadget}, + circuits::{ArithExtractor, AssignmentsExtractor, FCircuit}, + commitments::CommitmentDef, relations::WitnessInstanceSampler, - traits::{CF1, CF2, Dummy, Inputize, InputizeEmulated, SonobeCurve, SonobeField}, - transcripts::{ - Absorbable, Transcript, - griffin::{GriffinParams, sponge::GriffinSponge}, - }, + traits::{CF1, CF2, Dummy, SonobeCurve}, + transcripts::Transcript, }; use crate::{ @@ -29,7 +26,7 @@ pub mod circuits; pub trait FoldingSchemeCycleFoldExt: GroupBasedFoldingSchemePrimary { - type CFConfig: CycleFoldConfig::Commitment>; + type CFConfig: CycleFoldConfig::Commitment>; const N_CYCLEFOLDS: usize; @@ -52,8 +49,8 @@ pub trait FoldingSchemeCycleFoldExt: Vec< Vec< EmulatedFieldVar< - ::Scalar, - CF2<::Commitment>, + ::Scalar, + CF2<::Commitment>, >, >, >, @@ -98,26 +95,26 @@ pub struct CycleFoldBasedIVC { impl IVC for CycleFoldBasedIVC where FS1: FoldingSchemeCycleFoldExt< - 1, - 1, - Arith: From::Commitment>>>, - Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + 1, + 1, + Arith: From::Commitment>>>, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, + >, >, - >, FS2: GroupBasedFoldingSchemeSecondary< 1, 1, - Arith: From::Commitment>>>, + Arith: From::Commitment>>>, Gadget: FoldingSchemeFullVerifierGadget<1, 1, VerifierKey = ()>, - VC: CommitmentDef< - Commitment: SonobeCurve::Scalar>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, >, - T: Transcript::Commitment>>, + T: Transcript::Commitment>>, { - type Field = ::Scalar; + type Field = ::Scalar; type Config = (FS1::Config, FS2::Config, T::Config); @@ -159,7 +156,6 @@ where arith2_config: arith2.config(), step_circuit, }; - let cs = ArithExtractor::new(); cs.execute_synthesizer(augmented_circuit)?; let new_arith1 = cs.arith::()?; diff --git a/crates/ivc/src/compilers/mod.rs b/crates/ivc/src/compilers/mod.rs index 764a51d10..41e86f249 100644 --- a/crates/ivc/src/compilers/mod.rs +++ b/crates/ivc/src/compilers/mod.rs @@ -1,2 +1 @@ pub mod cyclefold; - From 81a47c669e8c863c741ac8900f283a2c9a6981ce Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 16:19:13 +0800 Subject: [PATCH 61/93] Fix CI --- crates/fs/Cargo.toml | 3 +++ crates/ivc/Cargo.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 9d1860885..0a86a8ee4 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -24,6 +24,9 @@ sonobe-primitives = { workspace = true } ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } ark-pallas = { workspace = true, features = ["curve", "r1cs"] } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + [features] default = ["parallel"] parallel = [ diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml index 5924a988b..74d249ea0 100644 --- a/crates/ivc/Cargo.toml +++ b/crates/ivc/Cargo.toml @@ -22,6 +22,8 @@ sonobe-fs = { workspace = true } ark-bn254 = { workspace = true, features = ["curve", "r1cs"] } ark-grumpkin = { workspace = true, features = ["r1cs"] } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } [features] default = ["parallel"] From 4792dc4efc6aa5ee7721e13bf361cf7a115b8f76 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 16:44:30 +0800 Subject: [PATCH 62/93] Fix clippy --- crates/fs/src/definitions/algorithms.rs | 2 +- crates/fs/src/definitions/utils.rs | 4 +--- .../ivc/src/compilers/cyclefold/circuits.rs | 3 +-- crates/ivc/src/compilers/cyclefold/mod.rs | 16 +++++++-------- crates/ivc/src/lib.rs | 20 ++++++------------- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/crates/fs/src/definitions/algorithms.rs b/crates/fs/src/definitions/algorithms.rs index 607c83d64..8310ffa4a 100644 --- a/crates/fs/src/definitions/algorithms.rs +++ b/crates/fs/src/definitions/algorithms.rs @@ -32,7 +32,7 @@ pub trait FoldingSchemeProver: FoldingSchemeDef /// Here, the randomness source is controlled by `transcript`. The returned /// intermediate randomness is useful for the construction of CycleFold /// circuits in our CycleFold-based folding-to-IVC compiler. - #[allow(non_snake_case)] + #[allow(non_snake_case, clippy::type_complexity)] fn prove( pk: &::ProverKey, transcript: &mut impl Transcript, diff --git a/crates/fs/src/definitions/utils.rs b/crates/fs/src/definitions/utils.rs index 51e2dd49b..6c2017157 100644 --- a/crates/fs/src/definitions/utils.rs +++ b/crates/fs/src/definitions/utils.rs @@ -48,9 +48,7 @@ impl Absorbable for TaggedVec { } } -impl, const TAG: char> AbsorbableVar - for TaggedVec -{ +impl, const TAG: char> AbsorbableVar for TaggedVec { fn absorb_into(&self, dest: &mut Vec>) -> Result<(), SynthesisError> { self.0.absorb_into(dest) } diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index b0bdbcb79..8c44b290e 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -1,4 +1,3 @@ -use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, @@ -57,7 +56,7 @@ where FC: FCircuit::Scalar>, T: Transcript, { - #[allow(non_snake_case)] + #[allow(non_snake_case, clippy::too_many_arguments)] pub fn compute_next_state( &self, cs: ConstraintSystemRef, diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index fcfe007a8..7df70056d 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -38,7 +38,7 @@ pub trait FoldingSchemeCycleFoldExt: rho: Self::Challenge, ) -> Vec; - #[allow(non_snake_case)] + #[allow(non_snake_case, clippy::type_complexity)] fn to_cyclefold_inputs( Us: [::RU; M], us: [::IU; N], @@ -95,14 +95,14 @@ pub struct CycleFoldBasedIVC { impl IVC for CycleFoldBasedIVC where FS1: FoldingSchemeCycleFoldExt< - 1, - 1, - Arith: From::Commitment>>>, - Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, - CM: CommitmentDef< - Commitment: SonobeCurve::Scalar>, - >, + 1, + 1, + Arith: From::Commitment>>>, + Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, + CM: CommitmentDef< + Commitment: SonobeCurve::Scalar>, >, + >, FS2: GroupBasedFoldingSchemeSecondary< 1, 1, diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index befd8bf17..6a13c38b3 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -29,11 +29,13 @@ pub trait IVC { fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; + #[allow(clippy::type_complexity)] fn generate_keys>( pp: Self::PublicParam, step_circuit: &FC, ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + #[allow(clippy::type_complexity, clippy::too_many_arguments)] fn prove>( pk: &Self::ProverKey, step_circuit: &FC, @@ -74,7 +76,7 @@ impl<'a, FC: FCircuit, I: IVC> IVCStatefulProver<'a, FC, I> { i: 0, current_state: initial_state.clone(), initial_state, - current_proof: I::Proof::dummy(&pk), + current_proof: I::Proof::dummy(pk), pk, }) } @@ -85,8 +87,8 @@ impl<'a, FC: FCircuit, I: IVC> IVCStatefulProver<'a, FC, I> { rng: impl RngCore, ) -> Result { let (next_state, external_outputs, next_proof) = I::prove( - &self.pk, - &self.step_circuit, + self.pk, + self.step_circuit, self.i, &self.initial_state, &self.current_state, @@ -128,17 +130,7 @@ pub trait Decider { #[cfg(test)] mod tests { - use ark_bn254::{Fr, G1Projective as C1}; - use ark_crypto_primitives::sponge::{CryptographicSponge, poseidon::PoseidonSponge}; - use ark_grumpkin::Projective as C2; - use ark_std::{error::Error, rand::Rng, sync::Arc, test_rng}; - use sonobe_primitives::{ - arithmetizations::Arith, - circuits::utils::CircuitForTest, - commitments::pedersen::{Pedersen, PedersenEmulatedGadget, PedersenGadget}, - traits::Dummy, - transcripts::{Transcript, griffin::GriffinParams, poseidon::poseidon_canonical_config}, - }; + use ark_std::{error::Error, rand::Rng}; use super::*; From 1c6a8b2cac0d8ba449443cbd8d2fae00c74305c4 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 18:35:53 +0800 Subject: [PATCH 63/93] Declare `wasm-bindgen-test` as dev-dependency --- crates/fs/Cargo.toml | 5 ++++- crates/ivc/Cargo.toml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 0a86a8ee4..772f4e47d 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -27,8 +27,11 @@ ark-pallas = { workspace = true, features = ["curve", "r1cs"] } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] getrandom = { version = "0.2", features = ["js"] } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = { workspace = true } + [features] -default = ["parallel"] +default = [] parallel = [ "sonobe-primitives/parallel", ] diff --git a/crates/ivc/Cargo.toml b/crates/ivc/Cargo.toml index 74d249ea0..a57dda5d7 100644 --- a/crates/ivc/Cargo.toml +++ b/crates/ivc/Cargo.toml @@ -25,8 +25,11 @@ ark-grumpkin = { workspace = true, features = ["r1cs"] } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] getrandom = { version = "0.2", features = ["js"] } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] +wasm-bindgen-test = { workspace = true } + [features] -default = ["parallel"] +default = [] parallel = [ "sonobe-fs/parallel", ] \ No newline at end of file From 0b6490079d24019b7d783082c1a4fef3ece65941 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:10:33 +0800 Subject: [PATCH 64/93] Add FS & IVC docs --- crates/fs/src/definitions/algorithms.rs | 61 ++++++--- crates/fs/src/definitions/circuits.rs | 32 ++++- crates/fs/src/definitions/errors.rs | 21 ++++ crates/fs/src/definitions/instances.rs | 34 ++++- crates/fs/src/definitions/keys.rs | 14 +++ crates/fs/src/definitions/mod.rs | 79 ++++++++++-- crates/fs/src/definitions/utils.rs | 7 ++ crates/fs/src/definitions/variants.rs | 23 +++- crates/fs/src/definitions/witnesses.rs | 22 +++- crates/fs/src/lib.rs | 27 ++++ .../ivc/src/compilers/cyclefold/circuits.rs | 84 ++++++++----- crates/ivc/src/compilers/cyclefold/mod.rs | 85 ++++++++++--- crates/ivc/src/compilers/mod.rs | 6 + crates/ivc/src/lib.rs | 117 ++++++++++++++++-- 14 files changed, 520 insertions(+), 92 deletions(-) diff --git a/crates/fs/src/definitions/algorithms.rs b/crates/fs/src/definitions/algorithms.rs index 8310ffa4a..a4238a038 100644 --- a/crates/fs/src/definitions/algorithms.rs +++ b/crates/fs/src/definitions/algorithms.rs @@ -1,12 +1,18 @@ +//! Traits that define out-of-circuit widgets for folding scheme algorithms +//! (preprocessing, key generation, proof generation, proof verification, and +//! deciding). + use ark_std::{borrow::Borrow, rand::RngCore}; use sonobe_primitives::{relations::Relation, transcripts::Transcript}; use super::{FoldingSchemeDef, errors::Error, keys::DeciderKey}; +/// [`FoldingSchemePreprocessor`] is the trait for folding scheme preprocessor. pub trait FoldingSchemePreprocessor: FoldingSchemeDef { - /// The preprocessing method is a randomized algorithm that takes as input - /// the size bounds of the folding scheme, which are contained in the - /// `config` parameter, and outputs the public parameters. + /// [`FoldingSchemePreprocessor::preprocess`] defines the preprocessing + /// algorithm, which is a randomized algorithm that takes as input the + /// config / parameterization `config` of the folding scheme (e.g., size + /// bounds of the folding scheme) and outputs the public parameters. /// /// Here, the randomness source is controlled by `rng`. /// @@ -15,23 +21,32 @@ pub trait FoldingSchemePreprocessor: FoldingSchemeDef { fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; } +/// [`FoldingSchemeKeyGenerator`] is the trait for folding scheme key generator. pub trait FoldingSchemeKeyGenerator: FoldingSchemeDef { - /// The key generation method is a deterministic algorithm that takes as - /// input the public parameters `pp` and the constraint system `arith`, and - /// outputs a prover key and a verifier key. + /// [`FoldingSchemeKeyGenerator::generate_keys`] defines the key generation + /// algorithm, which is a deterministic algorithm that takes as input the + /// public parameters `pp` and the arithmetization `arith`, and outputs a + /// prover key and a verifier key. fn generate_keys(pp: Self::PublicParam, arith: Self::Arith) -> Result; } +/// [`FoldingSchemeProver`] is the trait for folding scheme prover. pub trait FoldingSchemeProver: FoldingSchemeDef { - /// The proof generation method is a deterministic algorithm that takes as - /// input the prover key `pk`, the transcript `transcript` between the - /// prover and the verifier, the first witness-instance pair `W`, `U`, the - /// second witness-instance pair `w`, `u`, and outputs the folded witness - /// and instance, the proof, and the (intermediate) randomness. + /// [`FoldingSchemeProver::prove`] defines the proof generation algorithm, + /// which is a (probably) randomized algorithm that takes as input the + /// prover key `pk`, the transcript `transcript` between the prover and the + /// verifier, `M` running witnesses `Ws`, `M` running instances `Us`, `N` + /// incoming witnesses `ws`, and `N` incoming instances `us`, and outputs + /// the folded witness and instance, the proof, and the challenges. + /// + /// Here, although the challenges can usually be derived by `transcript` and + /// thus do not necessarily need to be returned for verification, we still + /// have the prover return them explicitly so that they can be used for the + /// construction of CycleFold circuits in our CycleFold-based folding-to-IVC + /// compiler without re-deriving them from the transcript. /// - /// Here, the randomness source is controlled by `transcript`. The returned - /// intermediate randomness is useful for the construction of CycleFold - /// circuits in our CycleFold-based folding-to-IVC compiler. + /// The prover may further use `rng` as the randomness source, e.g., for + /// the hiding/zero-knowledge property. #[allow(non_snake_case, clippy::type_complexity)] fn prove( pk: &::ProverKey, @@ -44,7 +59,13 @@ pub trait FoldingSchemeProver: FoldingSchemeDef ) -> Result<(Self::RW, Self::RU, Self::Proof, Self::Challenge), Error>; } +/// [`FoldingSchemeVerifier`] is the trait for folding scheme verifier. pub trait FoldingSchemeVerifier: FoldingSchemeDef { + /// [`FoldingSchemeVerifier::verify`] defines the proof verification + /// algorithm, which is a deterministic algorithm that takes as input the + /// verifier key `vk`, the transcript `transcript` between the prover and + /// the verifier, `M` running instances `Us`, `N` incoming instances `us`, + /// and the proof `proof`, and outputs the folded instance. #[allow(non_snake_case)] fn verify( vk: &::VerifierKey, @@ -55,12 +76,23 @@ pub trait FoldingSchemeVerifier: FoldingSchemeDe ) -> Result; } +/// [`FoldingSchemeDecider`] is the trait for folding scheme decider. pub trait FoldingSchemeDecider: FoldingSchemeDef { + /// [`FoldingSchemeDecider::decide_running`] defines the deciding algorithm + /// for running witness-instance pairs, which is a deterministic algorithm + /// that takes as input the decider key `dk`, a running witness `W` and a + /// running instance `U`, and outputs whether the witness-instance pair + /// satisfies the running relation. #[allow(non_snake_case)] fn decide_running(dk: &Self::DeciderKey, W: &Self::RW, U: &Self::RU) -> Result<(), Error> { Relation::::check_relation(dk, W, U) } + /// [`FoldingSchemeDecider::decide_running`] defines the deciding algorithm + /// for incoming witness-instance pairs, which is a deterministic algorithm + /// that takes as input the decider key `dk`, an incoming witness `W` and an + /// incoming instance `U`, and outputs whether the witness-instance pair + /// satisfies the incoming relation. fn decide_incoming(dk: &Self::DeciderKey, w: &Self::IW, u: &Self::IU) -> Result<(), Error> { Relation::::check_relation(dk, w, u) } @@ -68,6 +100,7 @@ pub trait FoldingSchemeDecider: FoldingSchemeDef { impl FoldingSchemeDecider for FS {} +/// [`FoldingSchemeOps`] is a convenience super-trait bundling all algorithms. pub trait FoldingSchemeOps: FoldingSchemePreprocessor + FoldingSchemeKeyGenerator diff --git a/crates/fs/src/definitions/circuits.rs b/crates/fs/src/definitions/circuits.rs index 5c127dcd6..4b4402d02 100644 --- a/crates/fs/src/definitions/circuits.rs +++ b/crates/fs/src/definitions/circuits.rs @@ -1,11 +1,30 @@ +//! Traits that define in-circuit gadgets for folding scheme algorithms, mainly +//! for proof verification. + use ark_relations::gr1cs::SynthesisError; use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::TranscriptGadget}; use super::{FoldingSchemeDefGadget, algorithms::FoldingSchemeOps}; +/// [`FoldingSchemePartialVerifierGadget`] is the partial in-circuit verifier. +/// +/// For schemes that have circuit-unfriendly parts in their verification, the +/// implementation can choose to only implement this partial verifier gadget and +/// use some other techniques for the remaining verification work. +/// For example, group-based folding schemes can defer the expensive elliptic +/// curve operations on commitments to an external CycleFold circuit. pub trait FoldingSchemePartialVerifierGadget: - FoldingSchemeDefGadget> + FoldingSchemeDefGadget> { + /// [`FoldingSchemePartialVerifierGadget::verify_hinted`] defines the proof + /// verification gadget that matches its out-of-circuit widget + /// [`crate::FoldingSchemeVerifier::verify`]. + /// + /// The implementation is allowed to create hints for the missing parts of + /// the verification that are not performed inside the constraint system, + /// and it is unnecessary to constrain these hints inside the circuit. + /// However, it is the caller's responsibility to ensure that these hints + /// are later verified using other techniques (e.g., CycleFold helper). #[allow(non_snake_case)] fn verify_hinted( vk: &Self::VerifierKey, @@ -16,9 +35,20 @@ pub trait FoldingSchemePartialVerifierGadget: ) -> Result<(Self::RU, Self::Challenge), SynthesisError>; } +/// [`FoldingSchemeFullVerifierGadget`] is the full in-circuit verifier. +/// +/// Extends [`FoldingSchemePartialVerifierGadget`] by performing everything +/// required for proof verification inside the constraint system. pub trait FoldingSchemeFullVerifierGadget: FoldingSchemePartialVerifierGadget { + /// [`FoldingSchemeFullVerifierGadget::verify`] defines the proof + /// verification gadget that matches its out-of-circuit widget + /// [`crate::FoldingSchemeVerifier::verify`]. + /// + /// Unlike [`FoldingSchemePartialVerifierGadget::verify_hinted`], the + /// implementation is expected to perform all necessary verification steps + /// and constrain all required variables inside the circuit. #[allow(non_snake_case)] fn verify( vk: &Self::VerifierKey, diff --git a/crates/fs/src/definitions/errors.rs b/crates/fs/src/definitions/errors.rs index de9edfd35..d721a88dc 100644 --- a/crates/fs/src/definitions/errors.rs +++ b/crates/fs/src/definitions/errors.rs @@ -1,3 +1,5 @@ +//! Error definitions for folding schemes. + use ark_relations::gr1cs::SynthesisError; use sonobe_primitives::{ arithmetizations::Error as ArithError, commitments::Error as CommitmentError, @@ -5,24 +7,43 @@ use sonobe_primitives::{ }; use thiserror::Error; +/// [`Error`] enumerates possible errors during folding scheme operations. #[derive(Debug, Error)] pub enum Error { + /// [`Error::ArithError`] indicates an error from the underlying constraint + /// system. #[error(transparent)] ArithError(#[from] ArithError), + /// [`Error::CommitmentError`] indicates an error from the underlying + /// commitment scheme. #[error(transparent)] CommitmentError(#[from] CommitmentError), + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. #[error(transparent)] SynthesisError(#[from] SynthesisError), + /// [`Error::SumCheckError`] indicates an error from the underlying sumcheck + /// protocol. #[error(transparent)] SumCheckError(#[from] SumCheckError), + /// [`Error::Unsupported`] indicates that a certain use case is not + /// supported. #[error("Unsupported use case: {0}")] Unsupported(String), + /// [`Error::DomainCreationFailure`] indicates a failure in creating + /// evaluation domains. #[error("Failed to create domain")] DomainCreationFailure, + /// [`Error::IndivisibleByVanishingPoly`] indicates that a polynomial is + /// not divisible by the vanishing polynomial of a certain domain. #[error("Indivisible by vanishing polynomial")] IndivisibleByVanishingPoly, + /// [`Error::UnsatisfiedRelation`] indicates that a certain relation is not + /// satisfied. #[error("Unsatisfied relation: {0}")] UnsatisfiedRelation(String), + /// [`Error::InvalidPublicParameters`] indicates that the provided public + /// parameters are invalid. #[error("Invalid public parameters: {0}")] InvalidPublicParameters(String), } diff --git a/crates/fs/src/definitions/instances.rs b/crates/fs/src/definitions/instances.rs index f24a44c60..cd431c448 100644 --- a/crates/fs/src/definitions/instances.rs +++ b/crates/fs/src/definitions/instances.rs @@ -1,3 +1,5 @@ +//! Traits and abstractions for folding scheme instances. + use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, select::CondSelectGadget}; use ark_relations::gr1cs::{Namespace, SynthesisError}; use ark_std::fmt::Debug; @@ -10,17 +12,36 @@ use sonobe_primitives::{ use super::utils::TaggedVec; +/// [`FoldingInstance`] defines the operations that a folding scheme's instance +/// should support. pub trait FoldingInstance: Clone + Debug + PartialEq + Eq + Absorbable { + /// [`FoldingInstance::N_COMMITMENTS`] defines the number of commitments + /// contained in the instance. const N_COMMITMENTS: usize; - /// Returns the commitments contained in the committed instance. + /// [`FoldingInstance::commitments`] returns the commitments contained in + /// the instance. + // TODO (@winderica): consider the scenario where the instance has multiple + // commitments of different types. fn commitments(&self) -> Vec<&CM::Commitment>; + /// [`FoldingInstance::public_inputs`] returns the reference to the public + /// inputs contained in the instance. fn public_inputs(&self) -> &[CM::Scalar]; + /// [`FoldingInstance::public_inputs_mut`] returns the mutable reference to + /// the public inputs contained in the instance. fn public_inputs_mut(&mut self) -> &mut [CM::Scalar]; } +/// [`PlainInstance`] is a vector of field elements that are the statements / +/// public inputs to a constraint system. +/// We provide this type for folding schemes that support such simple instances, +/// enabling compatibility with the definition of accumulation schemes (i.e., +/// running x plain -> running). +/// +/// To distinguish it from the witness vector, we use a tagged vector with tag +/// `'u'` for it. pub type PlainInstance = TaggedVec; impl Dummy<&A> for PlainInstance { @@ -45,17 +66,24 @@ impl FoldingInstance for PlainInstance { } } +/// [`FoldingInstanceVar`] is the in-circuit variable of [`FoldingInstance`]. pub trait FoldingInstanceVar: AllocVar + GR1CSVar> + AbsorbableVar + CondSelectGadget { - /// Returns the commitments contained in the committed instance. + /// [`FoldingInstanceVar::commitments`] returns the commitments contained in + /// the instance variable. fn commitments(&self) -> Vec<&CM::CommitmentVar>; + /// [`FoldingInstanceVar::public_inputs`] returns the reference to the + /// public inputs contained in the instance variable. fn public_inputs(&self) -> &Vec; + /// [`FoldingInstanceVar::new_witness_with_public_inputs`] allocates a + /// folding instance in the circuit as a witness variable, with the given + /// pre-allocated public inputs. fn new_witness_with_public_inputs( cs: impl Into>, u: &Self::Value, @@ -81,4 +109,6 @@ impl FoldingInstanceVar for PlainInstanceVar = PlainInstance; diff --git a/crates/fs/src/definitions/keys.rs b/crates/fs/src/definitions/keys.rs index 230395e18..a71d7f0b7 100644 --- a/crates/fs/src/definitions/keys.rs +++ b/crates/fs/src/definitions/keys.rs @@ -1,11 +1,25 @@ +//! Traits and abstractions for folding scheme keys. + use sonobe_primitives::arithmetizations::ArithConfig; +/// [`DeciderKey`] defines the information that a folding scheme's decider key +/// should include or provide access to. pub trait DeciderKey { + /// [`DeciderKey::ProverKey`] is the type of the prover key contained in the + /// decider key. type ProverKey; + /// [`DeciderKey::VerifierKey`] is the type of the verifier key contained in + /// the decider key. type VerifierKey; + /// [`DeciderKey::ArithConfig`] is the constraint system configuration + /// associated with the folding scheme. type ArithConfig: ArithConfig; + /// [`DeciderKey::to_pk`] returns the reference to the prover key. fn to_pk(&self) -> &Self::ProverKey; + /// [`DeciderKey::to_vk`] returns the reference to the verifier key. fn to_vk(&self) -> &Self::VerifierKey; + /// [`DeciderKey::to_arith_config`] returns the reference to the constraint + /// system configuration. fn to_arith_config(&self) -> &Self::ArithConfig; } diff --git a/crates/fs/src/definitions/mod.rs b/crates/fs/src/definitions/mod.rs index 77d45952e..fe1225413 100644 --- a/crates/fs/src/definitions/mod.rs +++ b/crates/fs/src/definitions/mod.rs @@ -1,3 +1,6 @@ +//! Shared traits for folding schemes, including definitions of related +//! cryptographic objects and algorithms in and out of circuit. + pub mod algorithms; pub mod circuits; pub mod errors; @@ -23,16 +26,56 @@ use self::{ witnesses::FoldingWitness, }; +/// [`FoldingSchemeDef`] provides the core type definitions of a folding scheme. +/// +/// A folding scheme is a cryptographic primitive that folds multiple instances +/// of computations into a single instance while preserving the validity of the +/// computations. +/// More specifically, a folding scheme in general considers two relations `R1` +/// and `R2`. +/// The folding prover folds `M` witness-instance pairs satisfying `R1` and `N` +/// witness-instance pairs satisfying `R2` into a single witness-instance pair +/// satisfying `R1`, along with a proof that the folding was done correctly. +/// The folding verifier folds `M` instances of `R1` and `N` instances of `R2` +/// into a single instance of `R1` under the help of the proof. +/// +/// While folding schemes can be applied in various contexts, we primarily focus +/// on their use in constructing recursive proof systems, and thus we refer to +/// `R1` as the "running relation" and `R2` as the "incoming relation" in the +/// codebase. +/// A witness-instance pair `(W, U)` of type `(RW, RU)` for `R1` is called a +/// "running" witness-instance pair, while a witness-instance pair `(w, u)` of +/// type `(IW, IU)` for `R2` is called an "incoming" witness-instance pair. +/// +/// Different folding schemes support different running and incoming relations, +/// as well as the number of witness-instance pairs that can be folded at once. pub trait FoldingSchemeDef { + /// [`FoldingSchemeDef::CM`] is the commitment scheme used by the folding + /// scheme. type CM: CommitmentDef; + /// [`FoldingSchemeDef::RW`] is the type of running witness. type RW: FoldingWitness + for<'a> Dummy<&'a ::Config>; + /// [`FoldingSchemeDef::RU`] is the type of running instance. type RU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + /// [`FoldingSchemeDef::IW`] is the type of incoming witness. type IW: FoldingWitness + for<'a> Dummy<&'a ::Config>; + /// [`FoldingSchemeDef::IU`] is the type of incoming instance. type IU: FoldingInstance + for<'a> Dummy<&'a ::Config>; + /// [`FoldingSchemeDef::TranscriptField`] is the field type used in the + /// transcript of the folding scheme. type TranscriptField: SonobeField; + /// [`FoldingSchemeDef::Arith`] is the constraint system supported by the + /// folding scheme. type Arith: Arith::ArithConfig>; + /// [`FoldingSchemeDef::Config`] is the type of configuration required to + /// generate the public parameters of the folding scheme. type Config; + /// [`FoldingSchemeDef::PublicParam`] is the type of public parameters of + /// the folding scheme. type PublicParam; + /// [`FoldingSchemeDef::DeciderKey`] is the type of decider key of the + /// folding scheme, which is used to determine the satisfiability of a + /// witness-instance pair. type DeciderKey: DeciderKey + Clone + Relation @@ -44,32 +87,52 @@ pub trait FoldingSchemeDef { Source = AssignmentsOwned<::Scalar>, Error = Error, >; + /// [`FoldingSchemeDef::Challenge`] is the type of challenge generated + /// during the folding process. type Challenge; + /// [`FoldingSchemeDef::Proof`] is the type of proof generated by the + /// folding prover. type Proof: Clone + for<'a> Dummy<&'a ::Config>; } + +/// [`FoldingSchemeDefGadget`] specifies the in-circuit associated types for a +/// folding scheme gadget. pub trait FoldingSchemeDefGadget { - type Native: FoldingSchemeDef; + /// [`FoldingSchemeDefGadget::Widget`] points to the out-of-circuit folding + /// scheme widget. + type Widget: FoldingSchemeDef; - type CM: CommitmentDefGadget::CM>; - type RU: FoldingInstanceVar::RU>; - type IU: FoldingInstanceVar::IU>; + /// [`FoldingSchemeDefGadget::CM`] is the commitment scheme gadget. + type CM: CommitmentDefGadget::CM>; + /// [`FoldingSchemeDefGadget::RU`] is the type of in-circuit running + /// instance variable. + type RU: FoldingInstanceVar::RU>; + /// [`FoldingSchemeDefGadget::IU`] is the type of in-circuit incoming + /// instance variable. + type IU: FoldingInstanceVar::IU>; + /// [`FoldingSchemeDefGadget::VerifierKey`] is the type of in-circuit + /// verifier key variable. type VerifierKey; + /// [`FoldingSchemeDefGadget::Challenge`] is the type of in-circuit + /// challenge variable. type Challenge: AllocVar< - ::Challenge, + ::Challenge, ::ConstraintField, > + GR1CSVar< ::ConstraintField, - Value = ::Challenge, + Value = ::Challenge, >; + /// [`FoldingSchemeDefGadget::Proof`] is the type of in-circuit proof + /// variable. type Proof: AllocVar< - ::Proof, + ::Proof, ::ConstraintField, > + GR1CSVar< ::ConstraintField, - Value = ::Proof, + Value = ::Proof, >; } diff --git a/crates/fs/src/definitions/utils.rs b/crates/fs/src/definitions/utils.rs index 6c2017157..f4d27acb8 100644 --- a/crates/fs/src/definitions/utils.rs +++ b/crates/fs/src/definitions/utils.rs @@ -1,3 +1,5 @@ +//! Utility types shared across folding scheme definitions. + use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ GR1CSVar, @@ -13,6 +15,11 @@ use ark_std::{ }; use sonobe_primitives::transcripts::{Absorbable, AbsorbableVar}; +/// [`TaggedVec`] is a wrapper around a vector that additionally carries a +/// compile-time `char` tag. +/// +/// This is used to create nominally distinct vector types that are structurally +/// identical. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TaggedVec(pub Vec); diff --git a/crates/fs/src/definitions/variants.rs b/crates/fs/src/definitions/variants.rs index d8bcae599..b3655f37d 100644 --- a/crates/fs/src/definitions/variants.rs +++ b/crates/fs/src/definitions/variants.rs @@ -1,3 +1,6 @@ +//! Traits that define variants of folding schemes based on different underlying +//! mathematical structures. + use sonobe_primitives::{ commitments::{CommitmentDef, GroupBasedCommitment}, traits::CF2, @@ -8,15 +11,23 @@ use crate::{ FoldingSchemePartialVerifierGadget, }; +/// [`GroupBasedFoldingSchemePrimaryDef`] defines a folding scheme based on +/// groups (elliptic curves), whose transcript field is the scalar field of its +/// group-based commitment scheme. pub trait GroupBasedFoldingSchemePrimaryDef: FoldingSchemeDef< CM: GroupBasedCommitment, TranscriptField = <::CM as CommitmentDef>::Scalar, > { - type Gadget: FoldingSchemeDefGadget::Gadget2>; + /// [`GroupBasedFoldingSchemePrimaryDef::Gadget`] is the in-circuit gadget + /// that defines the folding scheme. + type Gadget: FoldingSchemeDefGadget::Gadget2>; } +/// [`GroupBasedFoldingSchemePrimary`] is a convenience trait that combines the +/// definition [`GroupBasedFoldingSchemePrimaryDef`] and operations +/// [`FoldingSchemeOps`]. pub trait GroupBasedFoldingSchemePrimary: GroupBasedFoldingSchemePrimaryDef> + FoldingSchemeOps @@ -28,15 +39,23 @@ impl GroupBasedFoldingSchemePrimary fo { } +/// [`GroupBasedFoldingSchemeSecondaryDef`] defines a folding scheme based on +/// groups (elliptic curves), whose transcript field is the base field of its +/// group-based commitment scheme. pub trait GroupBasedFoldingSchemeSecondaryDef: FoldingSchemeDef< CM: GroupBasedCommitment, TranscriptField = CF2<<::CM as CommitmentDef>::Commitment>, > { - type Gadget: FoldingSchemeDefGadget::Gadget1>; + /// [`GroupBasedFoldingSchemeSecondaryDef::Gadget`] is the in-circuit gadget + /// that defines the folding scheme. + type Gadget: FoldingSchemeDefGadget::Gadget1>; } +/// [`GroupBasedFoldingSchemeSecondary`] is a convenience trait that combines +/// the definition [`GroupBasedFoldingSchemeSecondaryDef`] and operations +/// [`FoldingSchemeOps`]. pub trait GroupBasedFoldingSchemeSecondary: GroupBasedFoldingSchemeSecondaryDef> + FoldingSchemeOps diff --git a/crates/fs/src/definitions/witnesses.rs b/crates/fs/src/definitions/witnesses.rs index 783bd3f1f..95da4b78b 100644 --- a/crates/fs/src/definitions/witnesses.rs +++ b/crates/fs/src/definitions/witnesses.rs @@ -1,3 +1,5 @@ +//! Traits and abstractions for folding scheme witnesses. + use ark_r1cs_std::{GR1CSVar, alloc::AllocVar}; use ark_std::fmt::Debug; use sonobe_primitives::{ @@ -8,14 +10,27 @@ use sonobe_primitives::{ use super::utils::TaggedVec; +/// [`FoldingWitness`] defines the operations that a folding scheme's witness +/// should support. pub trait FoldingWitness: Debug { + /// [`FoldingWitness::N_OPENINGS`] defines the number of openings contained + /// in the witness. const N_OPENINGS: usize; - /// Returns the reference to all openings contained in the witness, each - /// being a tuple of the values being committed to and the randomness. + /// [`FoldingWitness::openings`] returns the reference to all openings + /// contained in the witness, where each opening a tuple of the values being + /// committed to and the randomness used in the commitment. fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)>; } +/// [`PlainWitness`] is a vector of field elements that are the witnesses to a +/// constraint system. +/// We provide this type for folding schemes that support such simple witnesses, +/// enabling compatibility with the definition of accumulation schemes (i.e., +/// running x plain -> running). +/// +/// To distinguish it from the instance vector, we use a tagged vector with tag +/// `'w'` for it. pub type PlainWitness = TaggedVec; impl Dummy<&A> for PlainWitness { @@ -32,6 +47,7 @@ impl FoldingWitness for PlainWitness { } } +/// [`FoldingWitnessVar`] is the in-circuit variable of [`FoldingWitness`]. pub trait FoldingWitnessVar: AllocVar + GR1CSVar> @@ -44,4 +60,6 @@ impl FoldingWitnessVar for T where { } +/// [`PlainWitnessVar`] is the in-circuit variable of [`PlainWitness`]. +// TODO (@winderica): use a different tag? pub type PlainWitnessVar = PlainWitness; diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 0d92bed27..7ee5bf18c 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -1,3 +1,30 @@ +#![warn(missing_docs)] + +//! Folding scheme definition and implementations. +//! +//! This crate provides the traits for folding schemes, the out-of-circuit +//! widgets and the in-circuit gadgets of their algorithms, and their associated +//! structures (such as keys, instances, and witnesses) in [`definitions`]. +//! +//! Concrete constructions of the following folding schemes are then implemented +//! as submodules: +//! - [`Nova`](nova) +//! - [`HyperNova`](hypernova) +//! - [`Mova`](mova) +//! - [`Ova`](ova) +//! - [`ProtoGalaxy`](protogalaxy) +//! +//! Each scheme module mirrors the same directory layout: +//! - `algorithms/`: Implementations for the following algorithms: +//! - Preprocessing/Setup: [`FoldingSchemePreprocessor`] +//! - Key generation: [`FoldingSchemeKeyGenerator`] +//! - Proof generation: [`FoldingSchemeProver`] +//! - Proof verification: [`FoldingSchemeVerifier`] +//! - `circuits/`: In-circuit (partial / full) gadgets, mainly for verification. +//! - `instances/`: Instance types. +//! - `witnesses/`: Witness types. + + pub mod definitions; pub use self::definitions::{ diff --git a/crates/ivc/src/compilers/cyclefold/circuits.rs b/crates/ivc/src/compilers/cyclefold/circuits.rs index 8c44b290e..18ef8cd04 100644 --- a/crates/ivc/src/compilers/cyclefold/circuits.rs +++ b/crates/ivc/src/compilers/cyclefold/circuits.rs @@ -1,3 +1,6 @@ +//! Augmented and CycleFold circuits for the CycleFold-based IVC compiler. + +use ark_ff::PrimeField; use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, @@ -6,22 +9,22 @@ use ark_r1cs_std::{ fields::{FieldVar, fp::FpVar}, }; use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::marker::PhantomData; use sonobe_fs::{ FoldingInstanceVar, FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, GroupBasedFoldingSchemePrimary, GroupBasedFoldingSchemeSecondary, }; use sonobe_primitives::{ - algebra::Val, arithmetizations::Arith, circuits::FCircuit, commitments::CommitmentDef, - traits::{CF2, Dummy, SonobeCurve}, + traits::{Dummy, SonobeCurve}, transcripts::{Transcript, TranscriptGadget}, }; use crate::compilers::cyclefold::FoldingSchemeCycleFoldExt; +/// [`AugmentedCircuit`] defines an augmented version of the user's step circuit +/// which additionally verifies the folding proofs in-circuit. pub struct AugmentedCircuit< 'a, FS1: GroupBasedFoldingSchemePrimary<1, 1>, @@ -29,10 +32,10 @@ pub struct AugmentedCircuit< FC: FCircuit, T: Transcript, > { - pub hash_config: T::Config, - pub arith1_config: &'a ::Config, - pub arith2_config: &'a ::Config, - pub step_circuit: &'a FC, + pub(super) hash_config: T::Config, + pub(super) arith1_config: &'a ::Config, + pub(super) arith2_config: &'a ::Config, + pub(super) step_circuit: &'a FC, } impl<'a, FS1, FS2, FC, T> AugmentedCircuit<'a, FS1, FS2, FC, T> @@ -56,6 +59,9 @@ where FC: FCircuit::Scalar>, T: Transcript, { + /// [`AugmentedCircuit::compute_next_state`] invokes the step circuit on the + /// current state and external inputs to compute the next state and external + /// outputs, and it additionally verifies the folding proofs in-circuit. #[allow(non_snake_case, clippy::too_many_arguments)] pub fn compute_next_state( &self, @@ -95,6 +101,9 @@ where let cf_U = AllocVar::new_witness(cs.clone(), || Ok(cf_U))?; let cf_proofs = Vec::new_witness(cs.clone(), || Ok(cf_proofs))?; + // 1. Fold primary instances. + // 1.a. Derive the public input to the primary (augmented) circuit in + // the `i-1`-th step, which is `u.x = H(i, z_0, z_i, U, cf_U)`. let u_x = sponge .clone() .add(&i)? @@ -103,26 +112,47 @@ where .add(&U)? .add(&cf_U)? .get_field_element()?; + // 1.b. Construct the incoming instance `u` representing the `i-1`-th + // execution of primary (augmented) circuit with the derived public + // input. let u = FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), u, vec![u_x])?; + // 1.c. Fold the primary running instance `U` and incoming instance `u` + // using the provided proof to obtain the next running instance + // `UU`. let (UU, rho) = FS1::Gadget::verify_hinted(&(), &mut transcript, [&U], [&u], &proof)?; + // 1.d. If this is the base case (`i = 0`), then we should instead use + // the dummy running instance as the next running instance. let actual_UU = is_basecase.select(&U_dummy, &UU)?; + // 2. Fold secondary instances. let mut cf_UU = cf_U; for ((cf_u, cf_u_x), cf_proof) in cf_us .iter() + // 2.a. Derive the public inputs to the secondary (CycleFold) + // circuits in the `i`-th step, which are obtained by calling + // the implementation of `FoldingSchemeCycleFoldExt`. .zip(FS1::to_cyclefold_inputs([U], [u], UU, proof, rho)?) .zip(&cf_proofs) { + // 2.b. Construct the incoming instance `cf_u` representing the + // corresponding execution of secondary (CycleFold) circuit + // with the derived public inputs. let cf_u = FoldingInstanceVar::new_witness_with_public_inputs(cs.clone(), cf_u, cf_u_x)?; + // 2.c. Fold the secondary incoming instance `cf_u` into the running + // instance `cf_UU` using the provided proof. cf_UU = FS2::Gadget::verify(&(), &mut transcript, [&cf_UU], [&cf_u], cf_proof)?; } + // 2.d. If this is the base case (`i = 0`), then we should instead use + // the dummy running instance as the next running instance. let actual_cf_UU = is_basecase.select(&cf_U_dummy, &cf_UU)?; + // 3. Update state by invoking the step circuit. let (next_state, external_outputs) = self.step_circuit .generate_step_constraints(i, current_state, external_inputs)?; + // 4. Compute public input `uu.x = H(i+1, z_0, z_{i+1}, UU, cf_UU)`. let uu_x = sponge .clone() .add(&ii)? @@ -195,21 +225,18 @@ where } } -/// [`CycleFoldConfig`] controls the behavior of [`CycleFoldCircuit`]. -/// -/// Looking ahead, the circuit computes the random linear combination of points, -/// which is essentially done by iteratively computing `P = (P + p_i) * r_i`, -/// where `P` is the folded point, `p_i` is the input point, and `r_i` is the -/// randomness. -pub trait CycleFoldConfig: Sized + Default { - type C: SonobeCurve; - - /// `mark_point_as_public` marks a point as public. +/// [`CycleFoldCircuit`] is the trait describing the deferred verification of +/// the folding proofs which is now expressed as a circuit on the secondary +/// curve. +pub trait CycleFoldCircuit: Sized + Default { + /// [`CycleFoldCircuit::mark_point_as_public`] marks a point as public. /// /// The final vector of public inputs is shorter than the result of calling /// [`AllocVar::new_input`], because we only need the x and y coordinates of /// the point, but the `infinity` flag is not necessary. - fn mark_point_as_public(point: &::Var) -> Result<(), SynthesisError> { + fn mark_point_as_public>( + point: &V, + ) -> Result<(), SynthesisError> { for x in &point.to_constraint_field()?[..2] { // This line "converts" `x` from a witness to a public input. // Instead of directly modifying the constraint system, we explicitly @@ -225,20 +252,9 @@ pub trait CycleFoldConfig: Sized + Default { Ok(()) } - fn verify_point_rlc(&self, cs: ConstraintSystemRef>) - -> Result<(), SynthesisError>; -} - -#[derive(Debug, Clone, Default)] -pub struct CycleFoldCircuit { - _cfg: PhantomData, -} - -impl ConstraintSynthesizer> for CycleFoldCircuit { - fn generate_constraints( - self, - cs: ConstraintSystemRef>, - ) -> Result<(), SynthesisError> { - Cfg::default().verify_point_rlc(cs) - } + /// [`CycleFoldCircuit::verify_point_rlc`] verifies the deferred folding + /// proof in-circuit on the secondary curve, which is done by checking the + /// random linear combination of the commitments contained in the folding + /// instances. + fn verify_point_rlc(&self, cs: ConstraintSystemRef) -> Result<(), SynthesisError>; } diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 7df70056d..6dea6c308 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -1,3 +1,9 @@ +//! Implementation of the CycleFold-based IVC compiler. +//! +//! It turns any compatible folding scheme into a full IVC scheme by running the +//! primary circuit on one curve and a "CycleFold" circuit on the secondary +//! curve to handle emulated elliptic curve operations. + use ark_ff::Zero; use ark_relations::gr1cs::{ConstraintSystem, SynthesisError}; use ark_std::{borrow::Borrow, marker::PhantomData, rand::RngCore}; @@ -18,26 +24,38 @@ use sonobe_primitives::{ use crate::{ Error, IVC, - compilers::cyclefold::circuits::{AugmentedCircuit, CycleFoldCircuit, CycleFoldConfig}, + compilers::cyclefold::circuits::{AugmentedCircuit, CycleFoldCircuit}, }; pub mod circuits; +/// [`FoldingSchemeCycleFoldExt`] is the extension trait that a folding scheme +/// must implement to be used with the CycleFold compiler. pub trait FoldingSchemeCycleFoldExt: GroupBasedFoldingSchemePrimary { - type CFConfig: CycleFoldConfig::Commitment>; + /// [`FoldingSchemeCycleFoldExt::CFCircuit`] is the CycleFold circuit type + /// associated with the folding scheme. + type CFCircuit: CycleFoldCircuit::Commitment>>; + /// [`FoldingSchemeCycleFoldExt::N_CYCLEFOLDS`] specifies how many CycleFold + /// operations are needed to verify the primary folding scheme's proof. const N_CYCLEFOLDS: usize; + /// [`FoldingSchemeCycleFoldExt::to_cyclefold_circuits`] creates CycleFold + /// circuits for verifying the point RLCs needed by the folding scheme. #[allow(non_snake_case)] - fn to_cyclefold_configs( + fn to_cyclefold_circuits( Us: &[impl Borrow; M], us: &[impl Borrow; N], proof: &Self::Proof, rho: Self::Challenge, - ) -> Vec; + ) -> Vec; + /// [`FoldingSchemeCycleFoldExt::to_cyclefold_inputs`] computes the inputs + /// to CycleFold circuits. + /// + /// This will be called by the augmented circuit on the primary curve. #[allow(non_snake_case, clippy::type_complexity)] fn to_cyclefold_inputs( Us: [::RU; M], @@ -58,12 +76,14 @@ pub trait FoldingSchemeCycleFoldExt: >; } +/// [`Key`] is the prover / verifier key for the CycleFold-based IVC scheme. pub struct Key( pub FS1::DeciderKey, pub FS2::DeciderKey, pub T, ); +/// [`Proof`] is the proof produced by the CycleFold compiler. pub struct Proof( pub FS1::RW, pub FS1::RU, @@ -88,6 +108,16 @@ impl Dummy<&Key> f } } +/// [`CycleFoldBasedIVC`] is the main implementation of the IVC compiler based +/// on CycleFold. +/// +/// We consider two folding schemes `FS1` and `FS2`, where `FS1` is the folding +/// scheme on the primary curve and `FS2` is the folding scheme on the secondary +/// curve. +/// The user's step circuit is proven using `FS1`, and part of the verification +/// of `FS1`'s proof is offloaded to `FS2` using CycleFold. +/// +/// `T` is the transcript type used by the IVC prover and verifier. pub struct CycleFoldBasedIVC { _d: PhantomData<(FS1, FS2, T)>, } @@ -98,6 +128,10 @@ where 1, 1, Arith: From::Commitment>>>, + // TODO (@winderica): + // All folding schemes we currently support have an empty verifier + // key, so I used `()` here, but this should be generalized in the + // future. Gadget: FoldingSchemePartialVerifierGadget<1, 1, VerifierKey = ()>, CM: CommitmentDef< Commitment: SonobeCurve::Scalar>, @@ -141,24 +175,35 @@ where (pp1, pp2, hash_config): Self::PublicParam, step_circuit: &FC, ) -> Result<(Self::ProverKey, Self::VerifierKey), Error> { - let cyclefold_circuit = CycleFoldCircuit::::default(); - - let cs = ArithExtractor::new(); - cs.execute_synthesizer(cyclefold_circuit)?; - let arith2 = cs.arith::()?; + // Run the CycleFold circuit to extract the arithmetization on the + // secondary curve. + let arith2 = { + let cs = ArithExtractor::new(); + cs.execute_fn(|cs| FS1::CFCircuit::default().verify_point_rlc(cs))?; + cs.arith::()? + }; + // The augmented circuit depends on the configuration of itself. + // For instance, we are not aware of the number of constraints in the + // augmented circuit until we fix `arith1_config`, which requires us to + // provide the number of constraints in the augmented circuit. + // + // To break this circular dependency, we use a fixed-point iteration + // where we start from a default arithmetization and repeatedly update + // it until its configuration stabilizes. let mut arith1 = FS1::Arith::default(); loop { - let augmented_circuit = AugmentedCircuit:: { - hash_config: hash_config.clone(), - arith1_config: arith1.config(), - arith2_config: arith2.config(), - step_circuit, + let new_arith1 = { + let cs = ArithExtractor::new(); + cs.execute_synthesizer(AugmentedCircuit:: { + hash_config: hash_config.clone(), + arith1_config: arith1.config(), + arith2_config: arith2.config(), + step_circuit, + })?; + cs.arith::()? }; - let cs = ArithExtractor::new(); - cs.execute_synthesizer(augmented_circuit)?; - let new_arith1 = cs.arith::()?; if new_arith1.config() == arith1.config() { break; } @@ -219,10 +264,10 @@ where &mut rng, )?; - let cf_configs = FS1::to_cyclefold_configs(&[U], &[u], &proof, challenge); - for (i, cfg) in cf_configs.iter().enumerate() { + let cf_circuits = FS1::to_cyclefold_circuits(&[U], &[u], &proof, challenge); + for (i, cf_circuit) in cf_circuits.into_iter().enumerate() { let cs = AssignmentsExtractor::new(); - cs.execute_fn(|cs| cfg.verify_point_rlc(cs))?; + cs.execute_fn(|cs| cf_circuit.verify_point_rlc(cs))?; let (cf_w, cf_u) = dk2.sample(cs.assignments()?, &mut rng)?; diff --git a/crates/ivc/src/compilers/mod.rs b/crates/ivc/src/compilers/mod.rs index 41e86f249..80e579802 100644 --- a/crates/ivc/src/compilers/mod.rs +++ b/crates/ivc/src/compilers/mod.rs @@ -1 +1,7 @@ +//! Compilers that transform a folding scheme into a full IVC scheme. +//! +//! We currently provide a compiler based on CycleFold, and in the future there +//! may be other compilers such as the naive one (on a single curve) which fits +//! well with hash-based folding schemes and the two curves one. + pub mod cyclefold; diff --git a/crates/ivc/src/lib.rs b/crates/ivc/src/lib.rs index 6a13c38b3..790b4f534 100644 --- a/crates/ivc/src/lib.rs +++ b/crates/ivc/src/lib.rs @@ -1,40 +1,106 @@ +#![warn(missing_docs)] + +//! Incremental Verifiable Computation (IVC) abstractions. +//! +//! This crate provides the [`IVC`] trait, which describes the common +//! interface for all IVC constructions, and [compilers] that turn a folding +//! scheme into a full IVC scheme. + use ark_ff::PrimeField; use ark_relations::gr1cs::SynthesisError; use ark_std::rand::RngCore; -use sonobe_primitives::{circuits::FCircuit, traits::Dummy}; +use sonobe_fs::Error as FoldingError; +use sonobe_primitives::{arithmetizations::Error as ArithError, circuits::FCircuit, traits::Dummy}; use thiserror::Error; pub mod compilers; +/// [`Error`] enumerates possible errors during the IVC operations. #[derive(Debug, Error)] pub enum Error { + /// [`Error::ArithError`] indicates an error from the underlying constraint + /// system. #[error(transparent)] - ArithError(#[from] sonobe_primitives::arithmetizations::Error), + ArithError(#[from] ArithError), + /// [`Error::FoldingError`] indicates an error from the underlying folding + /// scheme. #[error(transparent)] - FoldingError(#[from] sonobe_fs::Error), + FoldingError(#[from] FoldingError), + /// [`Error::SynthesisError`] indicates an error during constraint + /// synthesis. #[error(transparent)] SynthesisError(#[from] SynthesisError), + /// [`Error::IVCVerificationFail`] indicates that the IVC verification has + /// failed. #[error("IVC verification failed")] IVCVerificationFail, } +/// [`IVC`] defines the interface of Incremental Verifiable Computation schemes. +/// It follows the general definition of proof/argument systems, with +/// preprocessing, key generation, proving, and verification algorithms. pub trait IVC { + /// [`IVC::Field`] defines the field over which the IVC scheme operates. type Field: PrimeField; + /// [`IVC::Config`] defines the configuration (e.g., the size of public + /// parameters) for the IVC scheme. type Config; + /// [`IVC::PublicParam`] defines the public parameters produced by + /// preprocessing. type PublicParam; + /// [`IVC::ProverKey`] defines the prover key type for the IVC scheme. + /// We parameterize it by the step circuit type `FC`, so that a prover key + /// for one step circuit cannot be used for another step circuit. type ProverKey; + /// [`IVC::VerifierKey`] defines the verifier key type for the IVC scheme. + /// We parameterize it by the step circuit type `FC`, so that a verifier key + /// for one step circuit cannot be used for another step circuit. type VerifierKey; + /// [`IVC::Proof`] defines the proof type for the IVC scheme. + /// We parameterize it by the step circuit type `FC`, so that a proof for + /// one step circuit cannot be used for another step circuit. type Proof: for<'a> Dummy<&'a Self::ProverKey>; + /// [`IVC::preprocess`] defines the preprocessing algorithm, which is a + /// randomized algorithm that takes as input the config / parameterization + /// `config` of the IVC scheme and outputs the public parameters. + /// + /// Here, the randomness source is controlled by `rng`. + /// + /// The security parameter is implicitly specified by the size of underlying + /// fields and groups. + /// + /// This is usually called once for the given configuration and can be + /// reused for generating multiple keys for different step circuits, as long + /// as the step circuits conform to the configuration. fn preprocess(config: Self::Config, rng: impl RngCore) -> Result; + /// [`IVC::generate_keys`] defines the key generation algorithm, which is a + /// deterministic algorithm that takes as input the public parameters `pp` + /// and the step circuit `step_circuit`, and outputs a prover key and a + /// verifier key. #[allow(clippy::type_complexity)] fn generate_keys>( pp: Self::PublicParam, step_circuit: &FC, ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + /// [`IVC::prove`] defines the proof updating algorithm, which is a + /// (probably) randomized algorithm that takes as input the prover key `pk`, + /// the step circuit `step_circuit`, the current step `i`, the initial state + /// `initial_state`, the current state `current_state`, the external inputs + /// `external_inputs`, and the current proof `current_proof`. + /// It executes the step circuit on the current state and external inputs, + /// and outputs its returned next state and external outputs, along with the + /// new proof. + /// + /// Here, `current_proof` attests that `current_state` is correctly derived + /// from `initial_state` after `i` steps of executing `step_circuit`, and + /// the returned next proof attests that the next state is correctly derived + /// from `initial_state` after `i+1` steps with the given `external_inputs`. + /// + /// The prover may further use `rng` as the randomness source. #[allow(clippy::type_complexity, clippy::too_many_arguments)] fn prove>( pk: &Self::ProverKey, @@ -47,6 +113,11 @@ pub trait IVC { rng: impl RngCore, ) -> Result<(FC::State, FC::ExternalOutputs, Self::Proof), Error>; + /// [`IVC::verify`] defines the proof verification algorithm, which is a + /// deterministic algorithm that takes as input the verifier key `vk`, the + /// current step `i`, the initial state `initial_state`, the current state + /// `current_state`, and the proof `proof`, and outputs `Ok(())` if the + /// proof is valid, or an error otherwise. fn verify>( vk: &Self::VerifierKey, i: usize, @@ -56,16 +127,23 @@ pub trait IVC { ) -> Result<(), Error>; } +/// [`IVCStatefulProver`] is a convenience struct that implements a stateful IVC +/// prover who maintains running state across iterations, so that the user does +/// not need to manually track and pass in the current state and proof at each +/// step. pub struct IVCStatefulProver<'a, FC: FCircuit, I: IVC> { - pub pk: &'a I::ProverKey, - pub step_circuit: &'a FC, - pub i: usize, - pub initial_state: FC::State, - pub current_state: FC::State, - pub current_proof: I::Proof, + pk: &'a I::ProverKey, + step_circuit: &'a FC, + i: usize, + initial_state: FC::State, + current_state: FC::State, + current_proof: I::Proof, } impl<'a, FC: FCircuit, I: IVC> IVCStatefulProver<'a, FC, I> { + /// [`IVCStatefulProver::new`] creates a new stateful IVC prover with the + /// given prover key `pk`, step circuit `step_circuit`, and initial state + /// `initial_state`. pub fn new( pk: &'a I::ProverKey, step_circuit: &'a FC, @@ -81,6 +159,8 @@ impl<'a, FC: FCircuit, I: IVC> IVCStatefulProver<'a, FC, I> { }) } + /// [`IVCStatefulProver::prove_step`] performs one step of proving, updating + /// the internal state and proof, and returning the external outputs. pub fn prove_step( &mut self, external_inputs: FC::ExternalInputs, @@ -103,20 +183,37 @@ impl<'a, FC: FCircuit, I: IVC> IVCStatefulProver<'a, FC, I> { } } +/// [`Decider`] defines a decider / proof-compression SNARK, which produces a +/// final succinct zero-knowledge proof from an IVC proof. +// TODO (@winderica): Still WIP pub trait Decider { + /// [`Decider::IVC`] defines the underlying IVC scheme that the decider + /// compiles. type IVC: IVC; + /// [`Decider::ProverKey`] defines the prover key type for the decider. type ProverKey; + /// [`Decider::VerifierKey`] defines the verifier key type for the decider. type VerifierKey; + /// [`Decider::Instance`] defines the instance type for the decider. type Instance; + /// [`Decider::Witness`] defines the witness type for the decider. type Witness; + /// [`Decider::Proof`] defines the proof type for the decider. type Proof; + /// [`Decider::preprocess_and_generate_keys`] preprocesses the IVC prover + /// key `ivc_pk` and generates the decider's prover key and verifier key. + /// + /// This can be seen as a SNARK with circuit-specific setup. + // TODO (@winderica): consider universal/transparent setup fn preprocess_and_generate_keys( ivc_pk: &::ProverKey, rng: impl RngCore, ) -> Result<(Self::ProverKey, Self::VerifierKey), Error>; + /// [`Decider::prove`] generates a decider proof from the given IVC proof + /// and instance/witness. fn prove( pk: &Self::ProverKey, w: &Self::Witness, @@ -124,6 +221,8 @@ pub trait Decider { rng: impl RngCore, ) -> Result; + /// [`Decider::verify`] verifies the decider proof against the given + /// instance. fn verify(vk: &Self::VerifierKey, x: &Self::Instance, proof: &Self::Proof) -> Result<(), Error>; } From 3fb63b5b3d42575709ebe65046fea2767290110c Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:48:16 +0800 Subject: [PATCH 65/93] Fmt --- crates/fs/src/definitions/circuits.rs | 2 +- crates/fs/src/definitions/mod.rs | 1 - crates/fs/src/lib.rs | 1 - crates/ivc/src/compilers/cyclefold/mod.rs | 2 +- crates/ivc/src/compilers/mod.rs | 2 +- 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/fs/src/definitions/circuits.rs b/crates/fs/src/definitions/circuits.rs index 4b4402d02..68d1be5f5 100644 --- a/crates/fs/src/definitions/circuits.rs +++ b/crates/fs/src/definitions/circuits.rs @@ -45,7 +45,7 @@ pub trait FoldingSchemeFullVerifierGadget: /// [`FoldingSchemeFullVerifierGadget::verify`] defines the proof /// verification gadget that matches its out-of-circuit widget /// [`crate::FoldingSchemeVerifier::verify`]. - /// + /// /// Unlike [`FoldingSchemePartialVerifierGadget::verify_hinted`], the /// implementation is expected to perform all necessary verification steps /// and constrain all required variables inside the circuit. diff --git a/crates/fs/src/definitions/mod.rs b/crates/fs/src/definitions/mod.rs index fe1225413..38a737027 100644 --- a/crates/fs/src/definitions/mod.rs +++ b/crates/fs/src/definitions/mod.rs @@ -96,7 +96,6 @@ pub trait FoldingSchemeDef { + for<'a> Dummy<&'a ::Config>; } - /// [`FoldingSchemeDefGadget`] specifies the in-circuit associated types for a /// folding scheme gadget. pub trait FoldingSchemeDefGadget { diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 7ee5bf18c..abfe146bc 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -24,7 +24,6 @@ //! - `instances/`: Instance types. //! - `witnesses/`: Witness types. - pub mod definitions; pub use self::definitions::{ diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index 6dea6c308..b48b66976 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -54,7 +54,7 @@ pub trait FoldingSchemeCycleFoldExt: /// [`FoldingSchemeCycleFoldExt::to_cyclefold_inputs`] computes the inputs /// to CycleFold circuits. - /// + /// /// This will be called by the augmented circuit on the primary curve. #[allow(non_snake_case, clippy::type_complexity)] fn to_cyclefold_inputs( diff --git a/crates/ivc/src/compilers/mod.rs b/crates/ivc/src/compilers/mod.rs index 80e579802..95113ac8c 100644 --- a/crates/ivc/src/compilers/mod.rs +++ b/crates/ivc/src/compilers/mod.rs @@ -1,5 +1,5 @@ //! Compilers that transform a folding scheme into a full IVC scheme. -//! +//! //! We currently provide a compiler based on CycleFold, and in the future there //! may be other compilers such as the naive one (on a single curve) which fits //! well with hash-based folding schemes and the two curves one. From a692460cd201832494c866179f11e332033ca91d Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 04:39:50 +0800 Subject: [PATCH 66/93] Refactor: start porting Nova --- crates/fs/src/lib.rs | 1 + crates/fs/src/nova/instance.rs | 89 +++++++++ crates/fs/src/nova/mod.rs | 318 +++++++++++++++++++++++++++++++++ crates/fs/src/nova/witness.rs | 53 ++++++ 4 files changed, 461 insertions(+) create mode 100644 crates/fs/src/nova/instance.rs create mode 100644 crates/fs/src/nova/mod.rs create mode 100644 crates/fs/src/nova/witness.rs diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index abfe146bc..3c5b7624a 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -24,6 +24,7 @@ //! - `instances/`: Instance types. //! - `witnesses/`: Witness types. +pub mod nova; pub mod definitions; pub use self::definitions::{ diff --git a/crates/fs/src/nova/instance.rs b/crates/fs/src/nova/instance.rs new file mode 100644 index 000000000..ec832215a --- /dev/null +++ b/crates/fs/src/nova/instance.rs @@ -0,0 +1,89 @@ +use ark_ff::PrimeField; +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, + transcripts::Absorbable, +}; + +use crate::FoldingInstance; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningInstance { + pub cm_e: CM::Commitment, + pub u: CM::Scalar, + pub cm_w: CM::Commitment, + pub x: Vec, +} + +impl FoldingInstance for RunningInstance { + const N_COMMITMENTS: usize = 2; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm_e, &self.cm_w] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for RunningInstance { + fn dummy(cfg: &Cfg) -> Self { + Self { + cm_e: Default::default(), + u: Default::default(), + cm_w: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs()], + } + } +} + +impl Absorbable for RunningInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.u.absorb_into(dest); + self.x.absorb_into(dest); + self.cm_e.absorb_into(dest); + self.cm_w.absorb_into(dest); + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IncomingInstance { + pub cm_w: CM::Commitment, + pub x: Vec, +} + +impl FoldingInstance for IncomingInstance { + const N_COMMITMENTS: usize = 1; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm_w] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for IncomingInstance { + fn dummy(cfg: &Cfg) -> Self { + Self { + cm_w: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs()], + } + } +} + +impl Absorbable for IncomingInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.x.absorb_into(dest); + self.cm_w.absorb_into(dest); + } +} diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs new file mode 100644 index 000000000..f47e182cb --- /dev/null +++ b/crates/fs/src/nova/mod.rs @@ -0,0 +1,318 @@ +use ark_ff::{BigInteger, Field, One, PrimeField}; +use ark_std::{ + UniformRand, + borrow::Borrow, + cfg_into_iter, cfg_iter, + marker::PhantomData, + ops::Mul, + rand::{RngCore, rngs::mock::StepRng}, + sync::Arc, +}; +use instance::{IncomingInstance as IU, RunningInstance as RU}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + arithmetizations::{ + Arith, ArithConfig, ArithRelation, + r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, + }, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, + relations::{Relation, WitnessInstanceSampler}, + traits::{CF1, SonobeCurve, SonobeField}, + transcripts::Transcript, +}; +use witness::{IncomingWitness as IW, RunningWitness as RW}; + +use crate::{ + DeciderKey, Error, FoldingSchemeDecider, FoldingSchemeDef, FoldingSchemeKeyGenerator, + FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, +}; + +pub mod instance; +pub mod witness; + +#[derive(Clone)] +pub struct NovaKey { + arith: Arc, + ck: Arc, +} + +impl DeciderKey for NovaKey { + type ProverKey = Self; + type VerifierKey = (); + type ArithConfig = A::Config; + + fn to_pk(&self) -> &Self::ProverKey { + self + } + + fn to_vk(&self) -> &Self::VerifierKey { + &() + } + + fn to_arith_config(&self) -> &Self::ArithConfig { + self.arith.config() + } +} + +impl Relation, RU> for NovaKey +where + A: for<'a> ArithRelation, RelaxedInstance<&'a [CM::Scalar]>>, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &RW, u: &RU) -> Result<(), Self::Error> { + self.arith.check_relation( + &RelaxedWitness { w: &w.w, e: &w.e }, + &RelaxedInstance { x: &u.x, u: &u.u }, + )?; + CM::open(&self.ck, &w.w, &w.r_w, &u.cm_w)?; + CM::open(&self.ck, &w.e, &w.r_e, &u.cm_e)?; + Ok(()) + } +} + +impl Relation, IU> for NovaKey +where + A: ArithRelation, Vec>, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &IW, u: &IU) -> Result<(), Self::Error> { + self.arith.check_relation(&w.w, &u.x)?; + CM::open(&self.ck, &w.w, &w.r_w, &u.cm_w)?; + Ok(()) + } +} + +impl WitnessInstanceSampler, IU> for NovaKey { + type Source = AssignmentsOwned; + type Error = Error; + + fn sample(&self, z: Self::Source, rng: impl RngCore) -> Result<(IW, IU), Error> { + let (w, x) = (z.private, z.public); + let (cm_w, r_w) = CM::commit(&self.ck, &w, rng)?; + Ok((IW { w, r_w }, IU { cm_w, x })) + } +} + +impl WitnessInstanceSampler, RU> for NovaKey +where + A: for<'a> ArithRelation< + RelaxedWitness<&'a [CM::Scalar]>, + RelaxedInstance<&'a [CM::Scalar]>, + Evaluation = Vec, + >, + CM: CommitmentOps, +{ + type Source = (); + type Error = Error; + + fn sample(&self, _: Self::Source, mut rng: impl RngCore) -> Result<(RW, RU), Error> { + let cfg = self.arith.config(); + + let u = CM::Scalar::rand(&mut rng); + let x = (0..cfg.n_public_inputs()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let w = (0..cfg.n_witnesses()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let e = self.arith.eval_relation( + &RelaxedWitness { w: &w, e: &[] }, + &RelaxedInstance { x: &x, u: &u }, + )?; + + let (cm_w, r_w) = CM::commit(&self.ck, &w, &mut rng)?; + let (cm_e, r_e) = CM::commit(&self.ck, &e, &mut rng)?; + Ok((RW { w, r_w, e, r_e }, RU { cm_w, x, cm_e, u })) + } +} + +// used for the RO challenges. +// From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness +// error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this +// case, we keep the size of S close to 2^128. +pub struct Nova { + _CM: PhantomData, +} + +impl FoldingSchemeDef + for Nova +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = IW; + type IU = IU; + + type TranscriptField = CM::Scalar; + type Arith = R1CS; + + type Config = usize; + type PublicParam = CM::Key; + type DeciderKey = NovaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = CM::Commitment; +} + +impl FoldingSchemePreprocessor + for Nova +{ + fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(ck_len, &mut rng)?; + Ok(ck) + } +} + +impl FoldingSchemeKeyGenerator + for Nova +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + let cfg = r1cs.config(); + if ck.max_scalars_len() < cfg.n_constraints().max(cfg.n_witnesses()) { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the R1CS instance".into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} + +impl FoldingSchemeProver<1, 1> + for Nova +{ + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + _rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Compute the cross term `T` by following the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u.x).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w.w).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + + // Use `StepRng::new(0, 0)`, which is a dummy RNG that always generates + // 0 for the randomness (i.e., `r_T = 0`), no matter whether `CM` itself + // is hiding or not. + // + // This is because in Nova, we don't need hiding property for commitment + // to `T`. + let (cm_t, r_t) = CM::commit(&pk.ck, &t, StepRng::new(0, 0))?; + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + + Ok(( + RW { + e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), + r_e: W.r_e + r_t * rho, + w: cfg_iter!(W.w).zip(&w.w).map(|(a, b)| rho * b + a).collect(), + r_w: W.r_w + w.r_w * rho, + }, + RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + u.cm_w.mul(rho), + x: cfg_iter!(U.x).zip(&u.x).map(|(a, b)| rho * b + a).collect(), + }, + cm_t, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeVerifier<1, 1> + for Nova +{ + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + cm_t: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + + Ok(RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + u.cm_w.mul(rho), + x: cfg_iter!(U.x).zip(&u.x).map(|(a, b)| rho * b + a).collect(), + }) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective}; + use ark_ff::UniformRand; + use ark_std::{error::Error, test_rng}; + use sonobe_primitives::{ + circuits::utils::{CircuitForTest, satisfying_assignments_for_test}, + commitments::pedersen::Pedersen, + }; + + use super::*; + use crate::tests::test_folding_scheme; + + #[test] + fn test_nova() -> Result<(), Box> { + let mut rng = test_rng(); + + test_folding_scheme::>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..10) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..10) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + Ok(()) + } +} diff --git a/crates/fs/src/nova/witness.rs b/crates/fs/src/nova/witness.rs new file mode 100644 index 000000000..57dd4b176 --- /dev/null +++ b/crates/fs/src/nova/witness.rs @@ -0,0 +1,53 @@ +use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; + +use crate::FoldingWitness; + +#[derive(Debug, PartialEq)] +pub struct RunningWitness { + pub e: Vec, + pub r_e: CM::Randomness, + pub w: Vec, + pub r_w: CM::Randomness, +} + +impl FoldingWitness for RunningWitness { + const N_OPENINGS: usize = 2; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.e, &self.r_e), (&self.w, &self.r_w)] + } +} + +impl Dummy<&Cfg> for RunningWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + e: vec![Default::default(); cfg.n_constraints()], + r_e: Default::default(), + w: vec![Default::default(); cfg.n_witnesses()], + r_w: Default::default(), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct IncomingWitness { + pub w: Vec, + pub r_w: CM::Randomness, +} + +impl FoldingWitness for IncomingWitness { + const N_OPENINGS: usize = 1; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.w, &self.r_w)] + } +} + +impl Dummy<&Cfg> for IncomingWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses()], + r_w: Default::default(), + } + } +} From c9e7dce73f5a575591afd39beae7db16ed41ae45 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 18:43:27 +0800 Subject: [PATCH 67/93] Refactor: Allow Nova to have any transcript field --- crates/fs/src/nova/mod.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index f47e182cb..ee3c59597 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -136,12 +136,15 @@ where // From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness // error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this // case, we keep the size of S close to 2^128. -pub struct Nova { - _CM: PhantomData, +pub struct AbstractNova { + _t: PhantomData<(CM, TF)>, } -impl FoldingSchemeDef - for Nova +pub type Nova = + AbstractNova::Scalar, CHALLENGE_BITS>; + +impl FoldingSchemeDef + for AbstractNova { type CM = CM; type RW = RW; From e294f003c1715bd80523457e890bd8bbf4cf9fc0 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 19:04:22 +0800 Subject: [PATCH 68/93] Prepare for cyclefold --- crates/fs/src/nova/mod.rs | 55 ++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index ee3c59597..34745c357 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -1,3 +1,4 @@ +use ark_ec::CurveGroup; use ark_ff::{BigInteger, Field, One, PrimeField}; use ark_std::{ UniformRand, @@ -19,7 +20,7 @@ use sonobe_primitives::{ circuits::AssignmentsOwned, commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, - traits::{CF1, SonobeCurve, SonobeField}, + traits::{CF1, CF2, SonobeCurve, SonobeField}, transcripts::Transcript, }; use witness::{IncomingWitness as IW, RunningWitness as RW}; @@ -143,6 +144,9 @@ pub struct AbstractNova { pub type Nova = AbstractNova::Scalar, CHALLENGE_BITS>; +pub type CycleFoldNova = + AbstractNova::Commitment>, CHALLENGE_BITS>; + impl FoldingSchemeDef for AbstractNova { @@ -152,7 +156,7 @@ impl Fol type IW = IW; type IU = IU; - type TranscriptField = CM::Scalar; + type TranscriptField = TF; type Arith = R1CS; type Config = usize; @@ -162,8 +166,8 @@ impl Fol type Proof = CM::Commitment; } -impl FoldingSchemePreprocessor - for Nova +impl + FoldingSchemePreprocessor for AbstractNova { fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { let ck = CM::generate_key(ck_len, &mut rng)?; @@ -171,8 +175,8 @@ impl FoldingSchemePreproc } } -impl FoldingSchemeKeyGenerator - for Nova +impl + FoldingSchemeKeyGenerator for AbstractNova { fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { let ck = Arc::new(ck); @@ -187,12 +191,12 @@ impl FoldingSchemeKeyGene } } -impl FoldingSchemeProver<1, 1> - for Nova +impl + FoldingSchemeProver<1, 1> for AbstractNova { fn prove( pk: &NovaKey, - transcript: &mut impl Transcript, + transcript: &mut impl Transcript, Ws: &[impl Borrow; 1], Us: &[impl Borrow; 1], ws: &[impl Borrow; 1], @@ -249,12 +253,12 @@ impl FoldingSchemeProver< } } -impl FoldingSchemeVerifier<1, 1> - for Nova +impl FoldingSchemeVerifier<1, 1> + for AbstractNova { fn verify( _vk: &(), - transcript: &mut impl Transcript, + transcript: &mut impl Transcript, Us: &[impl Borrow; 1], us: &[impl Borrow; 1], cm_t: &Self::Proof<1, 1>, @@ -280,7 +284,7 @@ impl FoldingSchemeVerifie #[cfg(test)] mod tests { - use ark_bn254::{Fr, G1Projective}; + use ark_bn254::{Fq, Fr, G1Projective}; use ark_ff::UniformRand; use ark_std::{error::Error, test_rng}; use sonobe_primitives::{ @@ -291,31 +295,40 @@ mod tests { use super::*; use crate::tests::test_folding_scheme; - #[test] - fn test_nova() -> Result<(), Box> { - let mut rng = test_rng(); - - test_folding_scheme::>, 1, 1>( + fn test_nova_opt( + rounds: usize, + mut rng: impl RngCore, + ) -> Result<(), Box> { + test_folding_scheme::, TF>, 1, 1>( 8, CircuitForTest { x: Fr::rand(&mut rng), }, - (0..10) + (0..rounds) .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) .collect(), &mut rng, )?; - test_folding_scheme::>, 1, 1>( + test_folding_scheme::, TF>, 1, 1>( 8, CircuitForTest { x: Fr::rand(&mut rng), }, - (0..10) + (0..rounds) .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) .collect(), &mut rng, )?; Ok(()) } + + #[test] + fn test_nova() -> Result<(), Box> { + let mut rng = test_rng(); + + test_nova_opt::(10, &mut rng)?; + test_nova_opt::(10, &mut rng)?; + Ok(()) + } } From 086df2e05683fc57e2beef908c8891ec70c72a35 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 24 Oct 2025 22:38:47 +0800 Subject: [PATCH 69/93] Accumulation scheme compatible interface --- crates/fs/src/nova/mod.rs | 221 +++++++++++++++++++++++++++++++++++++- 1 file changed, 218 insertions(+), 3 deletions(-) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 34745c357..992d4e7c3 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -27,7 +27,8 @@ use witness::{IncomingWitness as IW, RunningWitness as RW}; use crate::{ DeciderKey, Error, FoldingSchemeDecider, FoldingSchemeDef, FoldingSchemeKeyGenerator, - FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, + FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as PU, + PlainWitness as PW, }; pub mod instance; @@ -89,6 +90,19 @@ where } } +impl Relation, PU> for NovaKey +where + A: ArithRelation, Vec>, + CM: CommitmentDef, +{ + type Error = Error; + + fn check_relation(&self, w: &PW, u: &PU) -> Result<(), Self::Error> { + self.arith.check_relation(w, u)?; + Ok(()) + } +} + impl WitnessInstanceSampler, IU> for NovaKey { type Source = AssignmentsOwned; type Error = Error; @@ -100,6 +114,21 @@ impl WitnessInstanceSampler, IU> for NovaKey WitnessInstanceSampler, PU> + for NovaKey +{ + type Source = AssignmentsOwned; + type Error = Error; + + fn sample( + &self, + z: Self::Source, + _rng: impl RngCore, + ) -> Result<(PW, PU), Error> { + Ok((z.private.into(), z.public.into())) + } +} + impl WitnessInstanceSampler, RU> for NovaKey where A: for<'a> ArithRelation< @@ -253,8 +282,8 @@ impl } } -impl FoldingSchemeVerifier<1, 1> - for AbstractNova +impl + FoldingSchemeVerifier<1, 1> for AbstractNova { fn verify( _vk: &(), @@ -282,6 +311,170 @@ impl Fol } } +// used for the RO challenges. +// From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness +// error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this +// case, we keep the size of S close to 2^128. +pub struct AbstractNova2 { + _t: PhantomData<(CM, TF)>, +} + +pub type Nova2 = + AbstractNova2::Scalar, CHALLENGE_BITS>; + +pub type CycleFoldNova2 = + AbstractNova2::Commitment>, CHALLENGE_BITS>; + +impl FoldingSchemeDef + for AbstractNova2 +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = PW; + type IU = PU; + + type TranscriptField = TF; + type Arith = R1CS; + + type Config = usize; + type PublicParam = CM::Key; + type DeciderKey = NovaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = (CM::Commitment, CM::Commitment); +} + +impl + FoldingSchemePreprocessor for AbstractNova2 +{ + fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(ck_len, &mut rng)?; + Ok(ck) + } +} + +impl + FoldingSchemeKeyGenerator for AbstractNova2 +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + let cfg = r1cs.config(); + if ck.max_scalars_len() < cfg.n_constraints().max(cfg.n_witnesses()) { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the R1CS instance".into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} + +impl + FoldingSchemeProver<1, 1> for AbstractNova2 +{ + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + mut rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Compute the cross term `T` by following the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u[..]).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w[..]).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + + let (cm_w, r_w) = CM::commit(&pk.ck, w, rng)?; + + // Use `StepRng::new(0, 0)`, which is a dummy RNG that always generates + // 0 for the randomness (i.e., `r_T = 0`), no matter whether `CM` itself + // is hiding or not. + // + // This is because in Nova, we don't need hiding property for commitment + // to `T`. + let (cm_t, r_t) = CM::commit(&pk.ck, &t, StepRng::new(0, 0))?; + + let pi = (cm_w, cm_t); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&pi); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + + Ok(( + RW { + e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), + r_e: W.r_e + r_t * rho, + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W.r_w + r_w * rho, + }, + RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + pi, + rho_bits.try_into().unwrap(), + )) + } +} + +impl + FoldingSchemeVerifier<1, 1> for AbstractNova2 +{ + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + pi: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(pi); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + + let (cm_w, cm_t) = pi; + + Ok(RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} + #[cfg(test)] mod tests { use ark_bn254::{Fq, Fr, G1Projective}; @@ -320,6 +513,28 @@ mod tests { .collect(), &mut rng, )?; + + test_folding_scheme::, TF>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::, TF>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; Ok(()) } From 0fb6edea554a7ecb37800b8b29f7ae81a75c7179 Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 25 Oct 2025 23:27:13 +0800 Subject: [PATCH 70/93] In circuit variables for Nova instances and witnesses --- crates/fs/src/nova/instance/circuits.rs | 216 ++++++++++++++++++ .../src/nova/{instance.rs => instance/mod.rs} | 2 + crates/fs/src/nova/witness/circuits.rs | 59 +++++ .../src/nova/{witness.rs => witness/mod.rs} | 2 + 4 files changed, 279 insertions(+) create mode 100644 crates/fs/src/nova/instance/circuits.rs rename crates/fs/src/nova/{instance.rs => instance/mod.rs} (99%) create mode 100644 crates/fs/src/nova/witness/circuits.rs rename crates/fs/src/nova/{witness.rs => witness/mod.rs} (98%) diff --git a/crates/fs/src/nova/instance/circuits.rs b/crates/fs/src/nova/instance/circuits.rs new file mode 100644 index 000000000..559e0d5dd --- /dev/null +++ b/crates/fs/src/nova/instance/circuits.rs @@ -0,0 +1,216 @@ +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::{ + commitments::{CommitmentDef, CommitmentDefGadget}, + transcripts::{Absorbable, AbsorbableVar}, +}; + +use super::{IncomingInstance, RunningInstance}; +use crate::{FoldingInstance, FoldingInstanceVar}; + +#[derive(Clone, Debug, PartialEq)] +pub struct RunningInstanceVar { + pub cm_e: CM::CommitmentVar, + pub u: CM::ScalarVar, + pub cm_w: CM::CommitmentVar, + pub x: Vec, +} + +impl AllocVar, CM::ConstraintField> + for RunningInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningInstance { cm_e, u, cm_w, x } = v.borrow(); + Ok(Self { + cm_e: AllocVar::new_variable(cs.clone(), || Ok(cm_e), mode)?, + u: AllocVar::new_variable(cs.clone(), || Ok(u), mode)?, + cm_w: AllocVar::new_variable(cs.clone(), || Ok(cm_w), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for RunningInstanceVar { + type Value = RunningInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.cm_e + .cs() + .or(self.u.cs()) + .or(self.cm_w.cs()) + .or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(RunningInstance { + cm_e: self.cm_e.value()?, + u: self.u.value()?, + cm_w: self.cm_w.value()?, + x: self.x.value()?, + }) + } +} + +impl AbsorbableVar for RunningInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.u.absorb_into(dest)?; + self.x.absorb_into(dest)?; + self.cm_e.absorb_into(dest)?; + self.cm_w.absorb_into(dest) + } +} + +impl CondSelectGadget for RunningInstanceVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.x.len() != false_value.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self { + cm_e: cond.select(&true_value.cm_e, &false_value.cm_e)?, + u: cond.select(&true_value.u, &false_value.u)?, + cm_w: cond.select(&true_value.cm_w, &false_value.cm_w)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + }) + } +} + +impl FoldingInstanceVar for RunningInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm_w, &self.cm_e] + } + + fn public_inputs(&self) -> &Vec { + &self.x + } + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result { + let cs = cs.into().cs(); + Ok(Self { + cm_e: AllocVar::new_witness(cs.clone(), || Ok(&u.cm_e))?, + u: AllocVar::new_witness(cs.clone(), || Ok(&u.u))?, + cm_w: AllocVar::new_witness(cs.clone(), || Ok(&u.cm_w))?, + x, + }) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct IncomingInstanceVar { + pub cm_w: CM::CommitmentVar, + pub x: Vec, +} + +impl AllocVar, CM::ConstraintField> + for IncomingInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let IncomingInstance { cm_w, x } = v.borrow(); + Ok(Self { + cm_w: AllocVar::new_variable(cs.clone(), || Ok(cm_w), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for IncomingInstanceVar { + type Value = IncomingInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.cm_w.cs().or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(IncomingInstance { + cm_w: self.cm_w.value()?, + x: self.x.value()?, + }) + } +} + +impl AbsorbableVar for IncomingInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.x.absorb_into(dest)?; + self.cm_w.absorb_into(dest) + } +} + +impl CondSelectGadget for IncomingInstanceVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.x.len() != false_value.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self { + cm_w: cond.select(&true_value.cm_w, &false_value.cm_w)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + }) + } +} + +impl FoldingInstanceVar for IncomingInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm_w] + } + + fn public_inputs(&self) -> &Vec { + &self.x + } + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result { + let cs = cs.into().cs(); + Ok(Self { + cm_w: AllocVar::new_witness(cs.clone(), || Ok(&u.cm_w))?, + x, + }) + } +} diff --git a/crates/fs/src/nova/instance.rs b/crates/fs/src/nova/instance/mod.rs similarity index 99% rename from crates/fs/src/nova/instance.rs rename to crates/fs/src/nova/instance/mod.rs index ec832215a..ba81b5838 100644 --- a/crates/fs/src/nova/instance.rs +++ b/crates/fs/src/nova/instance/mod.rs @@ -6,6 +6,8 @@ use sonobe_primitives::{ use crate::FoldingInstance; +pub mod circuits; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningInstance { pub cm_e: CM::Commitment, diff --git a/crates/fs/src/nova/witness/circuits.rs b/crates/fs/src/nova/witness/circuits.rs new file mode 100644 index 000000000..7cbe61d98 --- /dev/null +++ b/crates/fs/src/nova/witness/circuits.rs @@ -0,0 +1,59 @@ +use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; +use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::commitments::{CommitmentDef, CommitmentDefGadget}; + +use super::{IncomingWitness, RunningWitness}; +use crate::FoldingWitnessVar; + +#[derive(Debug, PartialEq)] +pub struct RunningWitnessVar { + pub e: Vec, + pub r_e: CM::RandomnessVar, + pub w: Vec, + pub r_w: CM::RandomnessVar, +} + +impl AllocVar, CM::ConstraintField> + for RunningWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningWitness { e, r_e, w, r_w } = v.borrow(); + Ok(Self { + e: AllocVar::new_variable(cs.clone(), || Ok(&e[..]), mode)?, + r_e: AllocVar::new_variable(cs.clone(), || Ok(r_e), mode)?, + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r_w: AllocVar::new_variable(cs.clone(), || Ok(r_w), mode)?, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct IncomingWitnessVar { + pub w: Vec, + pub r_w: CM::RandomnessVar, +} + +impl AllocVar, CM::ConstraintField> + for IncomingWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let IncomingWitness { w, r_w } = v.borrow(); + Ok(Self { + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r_w: AllocVar::new_variable(cs.clone(), || Ok(r_w), mode)?, + }) + } +} diff --git a/crates/fs/src/nova/witness.rs b/crates/fs/src/nova/witness/mod.rs similarity index 98% rename from crates/fs/src/nova/witness.rs rename to crates/fs/src/nova/witness/mod.rs index 57dd4b176..f98f49bf8 100644 --- a/crates/fs/src/nova/witness.rs +++ b/crates/fs/src/nova/witness/mod.rs @@ -2,6 +2,8 @@ use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDe use crate::FoldingWitness; +pub mod circuits; + #[derive(Debug, PartialEq)] pub struct RunningWitness { pub e: Vec, From d9214eea6eebdd72a15f59584b77cd62451f0cba Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 8 Nov 2025 08:47:34 +0800 Subject: [PATCH 71/93] Implement more traits for Nova instances & witnesses --- crates/fs/src/lib.rs | 2 +- crates/fs/src/nova/instance/circuits.rs | 7 ++-- crates/fs/src/nova/mod.rs | 20 ++++++----- crates/fs/src/nova/witness/circuits.rs | 46 ++++++++++++++++++++++--- crates/fs/src/nova/witness/mod.rs | 4 +-- 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 3c5b7624a..a347a133a 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -24,8 +24,8 @@ //! - `instances/`: Instance types. //! - `witnesses/`: Witness types. -pub mod nova; pub mod definitions; +pub mod nova; pub use self::definitions::{ FoldingSchemeDef, FoldingSchemeDefGadget, diff --git a/crates/fs/src/nova/instance/circuits.rs b/crates/fs/src/nova/instance/circuits.rs index 559e0d5dd..9a850957c 100644 --- a/crates/fs/src/nova/instance/circuits.rs +++ b/crates/fs/src/nova/instance/circuits.rs @@ -7,13 +7,10 @@ use ark_r1cs_std::{ }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::borrow::Borrow; -use sonobe_primitives::{ - commitments::{CommitmentDef, CommitmentDefGadget}, - transcripts::{Absorbable, AbsorbableVar}, -}; +use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::AbsorbableVar}; use super::{IncomingInstance, RunningInstance}; -use crate::{FoldingInstance, FoldingInstanceVar}; +use crate::FoldingInstanceVar; #[derive(Clone, Debug, PartialEq)] pub struct RunningInstanceVar { diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 992d4e7c3..0c28a61e1 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -1,5 +1,4 @@ -use ark_ec::CurveGroup; -use ark_ff::{BigInteger, Field, One, PrimeField}; +use ark_ff::{BigInteger, One, PrimeField}; use ark_std::{ UniformRand, borrow::Borrow, @@ -9,7 +8,6 @@ use ark_std::{ rand::{RngCore, rngs::mock::StepRng}, sync::Arc, }; -use instance::{IncomingInstance as IU, RunningInstance as RU}; #[cfg(feature = "parallel")] use rayon::prelude::*; use sonobe_primitives::{ @@ -20,15 +18,17 @@ use sonobe_primitives::{ circuits::AssignmentsOwned, commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, - traits::{CF1, CF2, SonobeCurve, SonobeField}, + traits::{CF2, SonobeField}, transcripts::Transcript, }; -use witness::{IncomingWitness as IW, RunningWitness as RW}; +use self::{ + instance::{IncomingInstance as IU, RunningInstance as RU}, + witness::{IncomingWitness as IW, RunningWitness as RW}, +}; use crate::{ - DeciderKey, Error, FoldingSchemeDecider, FoldingSchemeDef, FoldingSchemeKeyGenerator, - FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as PU, - PlainWitness as PW, + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeKeyGenerator, FoldingSchemePreprocessor, + FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as PU, PlainWitness as PW, }; pub mod instance; @@ -223,6 +223,7 @@ impl impl FoldingSchemeProver<1, 1> for AbstractNova { + #[allow(non_snake_case)] fn prove( pk: &NovaKey, transcript: &mut impl Transcript, @@ -285,6 +286,7 @@ impl impl FoldingSchemeVerifier<1, 1> for AbstractNova { + #[allow(non_snake_case)] fn verify( _vk: &(), transcript: &mut impl Transcript, @@ -372,6 +374,7 @@ impl impl FoldingSchemeProver<1, 1> for AbstractNova2 { + #[allow(non_snake_case)] fn prove( pk: &NovaKey, transcript: &mut impl Transcript, @@ -444,6 +447,7 @@ impl impl FoldingSchemeVerifier<1, 1> for AbstractNova2 { + #[allow(non_snake_case)] fn verify( _vk: &(), transcript: &mut impl Transcript, diff --git a/crates/fs/src/nova/witness/circuits.rs b/crates/fs/src/nova/witness/circuits.rs index 7cbe61d98..ee8554fd0 100644 --- a/crates/fs/src/nova/witness/circuits.rs +++ b/crates/fs/src/nova/witness/circuits.rs @@ -1,10 +1,12 @@ -use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; -use ark_relations::gr1cs::{Namespace, SynthesisError}; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::borrow::Borrow; -use sonobe_primitives::commitments::{CommitmentDef, CommitmentDefGadget}; +use sonobe_primitives::commitments::CommitmentDefGadget; use super::{IncomingWitness, RunningWitness}; -use crate::FoldingWitnessVar; #[derive(Debug, PartialEq)] pub struct RunningWitnessVar { @@ -34,6 +36,27 @@ impl AllocVar, CM::Constrain } } +impl GR1CSVar for RunningWitnessVar { + type Value = RunningWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.e + .cs() + .or(self.r_e.cs()) + .or(self.w.cs()) + .or(self.r_w.cs()) + } + + fn value(&self) -> Result { + Ok(RunningWitness { + e: self.e.value()?, + r_e: self.r_e.value()?, + w: self.w.value()?, + r_w: self.r_w.value()?, + }) + } +} + #[derive(Debug, PartialEq)] pub struct IncomingWitnessVar { pub w: Vec, @@ -57,3 +80,18 @@ impl AllocVar, CM::Constrai }) } } + +impl GR1CSVar for IncomingWitnessVar { + type Value = IncomingWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.w.cs().or(self.r_w.cs()) + } + + fn value(&self) -> Result { + Ok(IncomingWitness { + w: self.w.value()?, + r_w: self.r_w.value()?, + }) + } +} diff --git a/crates/fs/src/nova/witness/mod.rs b/crates/fs/src/nova/witness/mod.rs index f98f49bf8..583abfebd 100644 --- a/crates/fs/src/nova/witness/mod.rs +++ b/crates/fs/src/nova/witness/mod.rs @@ -4,7 +4,7 @@ use crate::FoldingWitness; pub mod circuits; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningWitness { pub e: Vec, pub r_e: CM::Randomness, @@ -31,7 +31,7 @@ impl Dummy<&Cfg> for RunningWitness { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct IncomingWitness { pub w: Vec, pub r_w: CM::Randomness, From b4de64fcf27becb08cb2ad7e6478b097ee1bc098 Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 17 Nov 2025 07:48:25 +0800 Subject: [PATCH 72/93] Nova CycleFold adapter --- crates/fs/src/nova/mod.rs | 150 +++++++++++++++-- .../src/compilers/cyclefold/adapters/mod.rs | 4 + .../src/compilers/cyclefold/adapters/nova.rs | 152 ++++++++++++++++++ crates/ivc/src/compilers/cyclefold/mod.rs | 1 + 4 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 crates/ivc/src/compilers/cyclefold/adapters/mod.rs create mode 100644 crates/ivc/src/compilers/cyclefold/adapters/nova.rs diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 0c28a61e1..1d674cb29 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -1,4 +1,6 @@ use ark_ff::{BigInteger, One, PrimeField}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, groups::CurveVar}; +use ark_relations::gr1cs::SynthesisError; use ark_std::{ UniformRand, borrow::Borrow, @@ -11,24 +13,32 @@ use ark_std::{ #[cfg(feature = "parallel")] use rayon::prelude::*; use sonobe_primitives::{ + algebra::ops::bits::FromBitsGadget, arithmetizations::{ Arith, ArithConfig, ArithRelation, r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, }, circuits::AssignmentsOwned, - commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, + commitments::{ + CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, GroupBasedCommitment, + }, relations::{Relation, WitnessInstanceSampler}, traits::{CF2, SonobeField}, - transcripts::Transcript, + transcripts::{Transcript, TranscriptGadget}, }; use self::{ - instance::{IncomingInstance as IU, RunningInstance as RU}, + instance::{ + IncomingInstance as IU, RunningInstance as RU, + circuits::{IncomingInstanceVar as IUVar, RunningInstanceVar as RUVar}, + }, witness::{IncomingWitness as IW, RunningWitness as RW}, }; use crate::{ - DeciderKey, Error, FoldingSchemeDef, FoldingSchemeKeyGenerator, FoldingSchemePreprocessor, - FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as PU, PlainWitness as PW, + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, + FoldingSchemeKeyGenerator, FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, + FoldingSchemeProver, FoldingSchemeVerifier, GroupBasedFoldingSchemePrimaryDef, + GroupBasedFoldingSchemeSecondaryDef, PlainInstance as PU, PlainWitness as PW, }; pub mod instance; @@ -317,14 +327,15 @@ impl // From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness // error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this // case, we keep the size of S close to 2^128. -pub struct AbstractNova2 { +// TODO: experimental design +struct AbstractNova2 { _t: PhantomData<(CM, TF)>, } -pub type Nova2 = +type Nova2 = AbstractNova2::Scalar, CHALLENGE_BITS>; -pub type CycleFoldNova2 = +type CycleFoldNova2 = AbstractNova2::Commitment>, CHALLENGE_BITS>; impl FoldingSchemeDef @@ -382,7 +393,7 @@ impl Us: &[impl Borrow; 1], ws: &[impl Borrow; 1], us: &[impl Borrow; 1], - mut rng: impl RngCore, + rng: impl RngCore, ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { let (W, U) = (Ws[0].borrow(), Us[0].borrow()); let (w, u) = (ws[0].borrow(), us[0].borrow()); @@ -479,6 +490,127 @@ impl } } +pub struct AbstractNovaGadget { + _vc: PhantomData, +} + +impl FoldingSchemeDefGadget + for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + type Widget = AbstractNova; + + type CM = CM; + type RU = RUVar; + type IU = IUVar; + type VerifierKey = (); + type Challenge = [Boolean; CHALLENGE_BITS]; + type Proof = CM::CommitmentVar; +} + +impl FoldingSchemePartialVerifierGadget<1, 1> + for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: CM::CommitmentVar::new_witness( + U.cm_e.cs().or(proof.cs()).or(rho.cs()), + || { + Ok(U.cm_e.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) + }, + )?, + cm_w: CM::CommitmentVar::new_witness( + U.cm_w.cs().or(u.cm_w.cs()).or(rho.cs()), + || { + Ok(U.cm_w.value().unwrap_or_default() + + u.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + }, + )?, + x: U.x + .iter() + .zip(&u.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeFullVerifierGadget<1, 1> + for AbstractNovaGadget +where + CM: CommitmentDefGadget, + CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, +{ + #[allow(non_snake_case)] + fn verify( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: proof.scalar_mul_le(rho_bits.iter())? + &U.cm_e, + cm_w: u.cm_w.scalar_mul_le(rho_bits.iter())? + &U.cm_w, + x: U.x + .iter() + .zip(&u.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} + +impl GroupBasedFoldingSchemePrimaryDef + for AbstractNova +{ + type Gadget = AbstractNovaGadget; +} + +impl GroupBasedFoldingSchemeSecondaryDef + for AbstractNova, CHALLENGE_BITS> +{ + type Gadget = AbstractNovaGadget; +} + #[cfg(test)] mod tests { use ark_bn254::{Fq, Fr, G1Projective}; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/mod.rs b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs new file mode 100644 index 000000000..cfbca4153 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs @@ -0,0 +1,4 @@ +//! Per-scheme adapters that implement [`super::CycleFoldCircuit`] for supported +//! folding schemes. + +pub mod nova; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs new file mode 100644 index 000000000..1a8d271de --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -0,0 +1,152 @@ +use ark_ff::{BigInteger, PrimeField, Zero}; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean}; +use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::{borrow::Borrow, iter::once}; +use sonobe_fs::{ + FoldingSchemeDefGadget, + nova::{CycleFoldNova, Nova}, +}; +use sonobe_primitives::{ + algebra::{ + field::emulated::{Bounds, EmulatedFieldVar}, + ops::bits::{FromBitsGadget, ToBitsGadgetExt}, + }, + commitments::GroupBasedCommitment, + traits::{CF2, SonobeCurve}, +}; + +use crate::compilers::cyclefold::{ + CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, +}; + +/// Configuration for Nova's CycleFold circuit +pub struct NovaCycleFoldCircuit { + r: Vec, + points: Vec, +} + +impl Default + for NovaCycleFoldCircuit +{ + fn default() -> Self { + Self { + r: vec![false; CHALLENGE_BITS], + points: vec![C::zero(); 2], + } + } +} + +impl CycleFoldCircuit> + for NovaCycleFoldCircuit +{ + fn verify_point_rlc(&self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let rho = FpVar::new_input(cs.clone(), || { + Ok(CF2::::from( + as PrimeField>::BigInt::from_bits_le(&self.r[..]), + )) + })?; + let rho_bits = rho.to_n_bits_le(CHALLENGE_BITS)?; + + let points = Vec::::new_witness(cs.clone(), || Ok(&self.points[..]))?; + for point in &points { + Self::mark_point_as_public(point)?; + } + + Self::mark_point_as_public(&(points[1].scalar_mul_le(rho_bits.iter())? + &points[0])) + } +} + +impl FoldingSchemeCycleFoldExt<1, 1> + for Nova +{ + const N_CYCLEFOLDS: usize = 2; + + type CFCircuit = NovaCycleFoldCircuit; + + fn to_cyclefold_circuits( + [U]: &[impl Borrow; 1], + [u]: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + rho: Self::Challenge, + ) -> Vec { + vec![ + NovaCycleFoldCircuit { + r: rho.into(), + points: vec![U.borrow().cm_e, *proof], + }, + NovaCycleFoldCircuit { + r: rho.into(), + points: vec![U.borrow().cm_w, u.borrow().cm_w], + }, + ] + } + + fn to_cyclefold_inputs( + [U]: [::RU; 1], + [u]: [::IU; 1], + UU: ::RU, + proof: ::Proof<1, 1>, + rho: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho = rho.to_vec(); + rho.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + let rho = EmulatedFieldVar::from_bounded_bits_le( + &rho, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?; + Ok(vec![ + once(rho.clone()) + .chain( + [U.cm_e, proof, UU.cm_e] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + once(rho) + .chain( + [U.cm_w, u.cm_w, UU.cm_w] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + ]) + } +} + +pub type NovaNovaIVC = + CycleFoldBasedIVC, CycleFoldNova, T>; + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective as C1}; + use ark_ff::UniformRand; + use ark_grumpkin::Projective as C2; + use ark_std::{error::Error, rand::thread_rng, sync::Arc}; + use sonobe_primitives::{ + circuits::utils::CircuitForTest, + commitments::pedersen::Pedersen, + transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, + }; + + use super::*; + use crate::tests::test_ivc; + + #[test] + fn test_nova_nova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + (65536, 2048, Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } +} diff --git a/crates/ivc/src/compilers/cyclefold/mod.rs b/crates/ivc/src/compilers/cyclefold/mod.rs index b48b66976..2a42bf0ba 100644 --- a/crates/ivc/src/compilers/cyclefold/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/mod.rs @@ -27,6 +27,7 @@ use crate::{ compilers::cyclefold::circuits::{AugmentedCircuit, CycleFoldCircuit}, }; +pub mod adapters; pub mod circuits; /// [`FoldingSchemeCycleFoldExt`] is the extension trait that a folding scheme From f2be215b9a08a9c50456c779cf1f169410516fe9 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 23:32:34 +0800 Subject: [PATCH 73/93] Hide cross term commitment in Nova --- crates/fs/src/nova/mod.rs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 1d674cb29..e11b72497 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -241,7 +241,7 @@ impl Us: &[impl Borrow; 1], ws: &[impl Borrow; 1], us: &[impl Borrow; 1], - _rng: impl RngCore, + rng: impl RngCore, ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { let (W, U) = (Ws[0].borrow(), Us[0].borrow()); let (w, u) = (ws[0].borrow(), us[0].borrow()); @@ -258,13 +258,7 @@ impl .map(|(a, b)| a - b) .collect::>(); - // Use `StepRng::new(0, 0)`, which is a dummy RNG that always generates - // 0 for the randomness (i.e., `r_T = 0`), no matter whether `CM` itself - // is hiding or not. - // - // This is because in Nova, we don't need hiding property for commitment - // to `T`. - let (cm_t, r_t) = CM::commit(&pk.ck, &t, StepRng::new(0, 0))?; + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; let rho_bits = { transcript.add(&U); @@ -393,7 +387,7 @@ impl Us: &[impl Borrow; 1], ws: &[impl Borrow; 1], us: &[impl Borrow; 1], - rng: impl RngCore, + mut rng: impl RngCore, ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { let (W, U) = (Ws[0].borrow(), Us[0].borrow()); let (w, u) = (ws[0].borrow(), us[0].borrow()); @@ -410,15 +404,9 @@ impl .map(|(a, b)| a - b) .collect::>(); - let (cm_w, r_w) = CM::commit(&pk.ck, w, rng)?; + let (cm_w, r_w) = CM::commit(&pk.ck, w, &mut rng)?; - // Use `StepRng::new(0, 0)`, which is a dummy RNG that always generates - // 0 for the randomness (i.e., `r_T = 0`), no matter whether `CM` itself - // is hiding or not. - // - // This is because in Nova, we don't need hiding property for commitment - // to `T`. - let (cm_t, r_t) = CM::commit(&pk.ck, &t, StepRng::new(0, 0))?; + let (cm_t, r_t) = CM::commit(&pk.ck, &t, &mut rng)?; let pi = (cm_w, cm_t); From c1fc8ff8cb362c802419ca8a5b8517c29cdf687b Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 22 Nov 2025 04:41:39 +0800 Subject: [PATCH 74/93] FS<2, 0> for Nova --- crates/fs/src/nova/mod.rs | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index e11b72497..7234c6d82 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -317,6 +317,109 @@ impl } } +impl + FoldingSchemeProver<2, 0> for AbstractNova +{ + #[allow(non_snake_case)] + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + [W1, W2]: &[impl Borrow; 2], + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + _: &[impl Borrow; 0], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<2, 0>, Self::Challenge), Error> { + let (W1, U1) = (W1.borrow(), U1.borrow()); + let (W2, U2) = (W2.borrow(), U2.borrow()); + + // Compute the cross term `T` by following the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U1.u + U2.u, + cfg_iter!(U1.x).zip(&U2.x).map(|(a, b)| *a + b).collect(), + cfg_iter!(W1.w).zip(&W2.w).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W1.e) + .zip(&W2.e) + .map(|((a, b), c)| a - b - c) + .collect::>(); + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; + + let rho_bits = { + transcript.add(&U1); + transcript.add(&U2); + transcript.add(&cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho_squared = rho * rho; + + Ok(( + RW { + e: cfg_iter!(W1.e) + .zip(&t) + .zip(&W2.e) + .map(|((a, b), c)| rho_squared * c + rho * b + a) + .collect(), + r_e: W1.r_e + r_t * rho + W2.r_e * rho_squared, + w: cfg_iter!(W1.w) + .zip(&W2.w) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W1.r_w + W2.r_w * rho, + }, + RU { + cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), + u: U1.u + rho * U2.u, + cm_w: U1.cm_w + U2.cm_w.mul(rho), + x: cfg_iter!(U1.x) + .zip(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }, + cm_t, + rho_bits.try_into().unwrap(), + )) + } +} + +impl + FoldingSchemeVerifier<2, 0> for AbstractNova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + cm_t: &Self::Proof<2, 0>, + ) -> Result { + let (U1, U2) = (U1.borrow(), U2.borrow()); + + let rho_bits = { + transcript.add(&U1); + transcript.add(&U2); + transcript.add(cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho_squared = rho * rho; + + Ok(RU { + cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), + u: U1.u + rho * U2.u, + cm_w: U1.cm_w + U2.cm_w.mul(rho), + x: cfg_iter!(U1.x) + .zip(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} + // used for the RO challenges. // From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness // error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this @@ -638,6 +741,28 @@ mod tests { &mut rng, )?; + test_folding_scheme::, TF>, 2, 0>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::, TF>, 2, 0>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + test_folding_scheme::, TF>, 1, 1>( 8, CircuitForTest { From d9f2cb445ce5b2fb421facd8b3588de426453993 Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 22 Nov 2025 05:40:41 +0800 Subject: [PATCH 75/93] Circuits for 2+0 Nova --- crates/fs/src/nova/mod.rs | 55 +++++++++++++ .../src/compilers/cyclefold/adapters/nova.rs | 80 ++++++++++++++++++- 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 7234c6d82..acad1cfa8 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -652,6 +652,61 @@ where } } +impl FoldingSchemePartialVerifierGadget<2, 0> + for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U1, U2]: [&Self::RU; 2], + _: [&Self::IU; 0], + proof: &Self::Proof<2, 0>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U1)?; + transcript.add(&U2)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + u: (U2.u.clone() * &rho + &U1.u) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: CM::CommitmentVar::new_witness( + U1.cm_e.cs().or(U2.cm_e.cs()).or(proof.cs()).or(rho.cs()), + || { + let rho = rho.value().unwrap_or_default(); + Ok(U1.cm_e.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho + + U2.cm_e.value().unwrap_or_default() * rho * rho) + }, + )?, + cm_w: CM::CommitmentVar::new_witness( + U1.cm_w.cs().or(U2.cm_w.cs()).or(rho.cs()), + || { + Ok(U1.cm_w.value().unwrap_or_default() + + U2.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + }, + )?, + x: U1 + .x + .iter() + .zip(&U2.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + impl FoldingSchemeFullVerifierGadget<1, 1> for AbstractNovaGadget where diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs index 1a8d271de..93613cc6e 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -1,5 +1,7 @@ use ark_ff::{BigInteger, PrimeField, Zero}; -use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean}; +use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean, +}; use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; use ark_std::{borrow::Borrow, iter::once}; use sonobe_fs::{ @@ -9,6 +11,7 @@ use sonobe_fs::{ use sonobe_primitives::{ algebra::{ field::emulated::{Bounds, EmulatedFieldVar}, + group::emulated::EmulatedAffineVar, ops::bits::{FromBitsGadget, ToBitsGadgetExt}, }, commitments::GroupBasedCommitment, @@ -116,6 +119,81 @@ impl FoldingSchemeCycleFo } } +impl FoldingSchemeCycleFoldExt<2, 0> + for Nova +{ + const N_CYCLEFOLDS: usize = 3; + + type CFCircuit = NovaCycleFoldCircuit; + + fn to_cyclefold_circuits( + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + proof: &Self::Proof<2, 0>, + rho_bits: Self::Challenge, + ) -> Vec { + let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + vec![ + NovaCycleFoldCircuit { + r: rho_bits.into(), + points: vec![*proof, U2.borrow().cm_e], + }, + NovaCycleFoldCircuit { + r: rho_bits.into(), + points: vec![U1.borrow().cm_e, U2.borrow().cm_e * rho + proof], + }, + NovaCycleFoldCircuit { + r: rho_bits.into(), + points: vec![U1.borrow().cm_w, U2.borrow().cm_w], + }, + ] + } + + fn to_cyclefold_inputs( + [U1, U2]: [::RU; 2], + _: [::IU; 0], + UU: ::RU, + proof: ::Proof<2, 0>, + rho_bits: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho_bits = rho_bits.to_vec(); + rho_bits.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + let rho = EmulatedFieldVar::from_bounded_bits_le( + &rho_bits, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?; + let x = + EmulatedAffineVar::new_witness(U2.cm_e.cs().or(proof.cs()).or(rho_bits.cs()), || { + let rho_bits = rho_bits.value().unwrap_or_default(); + let rho = + CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + Ok(proof.value().unwrap_or_default() + U2.cm_e.value().unwrap_or_default() * rho) + })?; + Ok(vec![ + once(rho.clone()) + .chain( + [proof, U2.cm_e, x.clone()] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + once(rho.clone()) + .chain([U1.cm_e, x, UU.cm_e].into_iter().flat_map(|p| [p.x, p.y])) + .collect(), + once(rho) + .chain( + [U1.cm_w, U2.cm_w, UU.cm_w] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + ]) + } +} + pub type NovaNovaIVC = CycleFoldBasedIVC, CycleFoldNova, T>; From 9ea5b521df8fa3c77fb7d2f0f780f53e1a1fa2a4 Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 24 Nov 2025 03:11:21 +0800 Subject: [PATCH 76/93] Cleanup --- crates/fs/src/nova/mod.rs | 25 ++++++++----------- .../src/compilers/cyclefold/adapters/nova.rs | 15 ++++------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index acad1cfa8..4dd3dd09f 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -1,19 +1,14 @@ -use ark_ff::{BigInteger, One, PrimeField}; +use ark_ff::One; use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, groups::CurveVar}; use ark_relations::gr1cs::SynthesisError; use ark_std::{ - UniformRand, - borrow::Borrow, - cfg_into_iter, cfg_iter, - marker::PhantomData, - ops::Mul, - rand::{RngCore, rngs::mock::StepRng}, - sync::Arc, + UniformRand, borrow::Borrow, cfg_into_iter, cfg_iter, marker::PhantomData, ops::Mul, + rand::RngCore, sync::Arc, }; #[cfg(feature = "parallel")] use rayon::prelude::*; use sonobe_primitives::{ - algebra::ops::bits::FromBitsGadget, + algebra::ops::bits::{FromBits, FromBitsGadget}, arithmetizations::{ Arith, ArithConfig, ArithRelation, r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, @@ -266,7 +261,7 @@ impl transcript.add(&cm_t); transcript.challenge_bits(CHALLENGE_BITS) }; - let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); Ok(( RW { @@ -306,7 +301,7 @@ impl transcript.add(&cm_t); transcript.challenge_bits(CHALLENGE_BITS) }; - let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); Ok(RU { cm_e: U.cm_e + cm_t.mul(rho), @@ -354,7 +349,7 @@ impl transcript.add(&cm_t); transcript.challenge_bits(CHALLENGE_BITS) }; - let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); let rho_squared = rho * rho; Ok(( @@ -405,7 +400,7 @@ impl transcript.add(cm_t); transcript.challenge_bits(CHALLENGE_BITS) }; - let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); let rho_squared = rho * rho; Ok(RU { @@ -519,7 +514,7 @@ impl transcript.add(&pi); transcript.challenge_bits(CHALLENGE_BITS) }; - let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); Ok(( RW { @@ -565,7 +560,7 @@ impl transcript.add(pi); transcript.challenge_bits(CHALLENGE_BITS) }; - let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); let (cm_w, cm_t) = pi; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs index 93613cc6e..c3cd4ae35 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -1,4 +1,4 @@ -use ark_ff::{BigInteger, PrimeField, Zero}; +use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean, }; @@ -12,7 +12,7 @@ use sonobe_primitives::{ algebra::{ field::emulated::{Bounds, EmulatedFieldVar}, group::emulated::EmulatedAffineVar, - ops::bits::{FromBitsGadget, ToBitsGadgetExt}, + ops::bits::{FromBits, FromBitsGadget, ToBitsGadgetExt}, }, commitments::GroupBasedCommitment, traits::{CF2, SonobeCurve}, @@ -43,11 +43,7 @@ impl CycleFoldCircuit> for NovaCycleFoldCircuit { fn verify_point_rlc(&self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let rho = FpVar::new_input(cs.clone(), || { - Ok(CF2::::from( - as PrimeField>::BigInt::from_bits_le(&self.r[..]), - )) - })?; + let rho = FpVar::new_input(cs.clone(), || Ok(CF2::::from_bits_le(&self.r[..])))?; let rho_bits = rho.to_n_bits_le(CHALLENGE_BITS)?; let points = Vec::::new_witness(cs.clone(), || Ok(&self.points[..]))?; @@ -132,7 +128,7 @@ impl FoldingSchemeCycleFo proof: &Self::Proof<2, 0>, rho_bits: Self::Challenge, ) -> Vec { - let rho = CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); vec![ NovaCycleFoldCircuit { r: rho_bits.into(), @@ -168,8 +164,7 @@ impl FoldingSchemeCycleFo let x = EmulatedAffineVar::new_witness(U2.cm_e.cs().or(proof.cs()).or(rho_bits.cs()), || { let rho_bits = rho_bits.value().unwrap_or_default(); - let rho = - CM::Scalar::from(::BigInt::from_bits_le(&rho_bits)); + let rho = CM::Scalar::from_bits_le(&rho_bits); Ok(proof.value().unwrap_or_default() + U2.cm_e.value().unwrap_or_default() * rho) })?; Ok(vec![ From 461720b8297b30dffc9146e8905ff3f8c4df1cdd Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 25 Nov 2025 00:52:37 +0800 Subject: [PATCH 77/93] Split impls into separate submodules --- .../fs/src/nova/algorithms/key_generator.rs | 43 ++ crates/fs/src/nova/algorithms/mod.rs | 4 + crates/fs/src/nova/algorithms/preprocessor.rs | 25 + crates/fs/src/nova/algorithms/prover.rs | 209 +++++++ crates/fs/src/nova/algorithms/verifier.rs | 111 ++++ crates/fs/src/nova/circuits/mod.rs | 1 + crates/fs/src/nova/circuits/verifier.rs | 158 ++++++ .../nova/{instance => instances}/circuits.rs | 0 .../src/nova/{instance => instances}/mod.rs | 0 crates/fs/src/nova/mod.rs | 518 +----------------- .../nova/{witness => witnesses}/circuits.rs | 0 .../fs/src/nova/{witness => witnesses}/mod.rs | 0 12 files changed, 561 insertions(+), 508 deletions(-) create mode 100644 crates/fs/src/nova/algorithms/key_generator.rs create mode 100644 crates/fs/src/nova/algorithms/mod.rs create mode 100644 crates/fs/src/nova/algorithms/preprocessor.rs create mode 100644 crates/fs/src/nova/algorithms/prover.rs create mode 100644 crates/fs/src/nova/algorithms/verifier.rs create mode 100644 crates/fs/src/nova/circuits/mod.rs create mode 100644 crates/fs/src/nova/circuits/verifier.rs rename crates/fs/src/nova/{instance => instances}/circuits.rs (100%) rename crates/fs/src/nova/{instance => instances}/mod.rs (100%) rename crates/fs/src/nova/{witness => witnesses}/circuits.rs (100%) rename crates/fs/src/nova/{witness => witnesses}/mod.rs (100%) diff --git a/crates/fs/src/nova/algorithms/key_generator.rs b/crates/fs/src/nova/algorithms/key_generator.rs new file mode 100644 index 000000000..7e49489ba --- /dev/null +++ b/crates/fs/src/nova/algorithms/key_generator.rs @@ -0,0 +1,43 @@ +use ark_std::sync::Arc; +use sonobe_primitives::{ + arithmetizations::{Arith, ArithConfig}, + commitments::{CommitmentKey, GroupBasedCommitment}, + traits::SonobeField, +}; + +use crate::{ + Error, FoldingSchemeKeyGenerator, + nova::{AbstractNova, AbstractNova2}, +}; + +impl + FoldingSchemeKeyGenerator for AbstractNova +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + let cfg = r1cs.config(); + if ck.max_scalars_len() < cfg.n_constraints().max(cfg.n_witnesses()) { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the R1CS instance".into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} + +impl + FoldingSchemeKeyGenerator for AbstractNova2 +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + let cfg = r1cs.config(); + if ck.max_scalars_len() < cfg.n_constraints().max(cfg.n_witnesses()) { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the R1CS instance".into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} diff --git a/crates/fs/src/nova/algorithms/mod.rs b/crates/fs/src/nova/algorithms/mod.rs new file mode 100644 index 000000000..b0960535b --- /dev/null +++ b/crates/fs/src/nova/algorithms/mod.rs @@ -0,0 +1,4 @@ +pub mod preprocessor; +pub mod key_generator; +pub mod prover; +pub mod verifier; diff --git a/crates/fs/src/nova/algorithms/preprocessor.rs b/crates/fs/src/nova/algorithms/preprocessor.rs new file mode 100644 index 000000000..916aa08bd --- /dev/null +++ b/crates/fs/src/nova/algorithms/preprocessor.rs @@ -0,0 +1,25 @@ +use ark_std::rand::RngCore; +use sonobe_primitives::{commitments::GroupBasedCommitment, traits::SonobeField}; + +use crate::{ + nova::{AbstractNova, AbstractNova2}, + Error, FoldingSchemePreprocessor, +}; + +impl + FoldingSchemePreprocessor for AbstractNova +{ + fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(ck_len, &mut rng)?; + Ok(ck) + } +} + +impl + FoldingSchemePreprocessor for AbstractNova2 +{ + fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(ck_len, &mut rng)?; + Ok(ck) + } +} diff --git a/crates/fs/src/nova/algorithms/prover.rs b/crates/fs/src/nova/algorithms/prover.rs new file mode 100644 index 000000000..3e163b878 --- /dev/null +++ b/crates/fs/src/nova/algorithms/prover.rs @@ -0,0 +1,209 @@ +use ark_ff::One; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, ops::Mul, rand::RngCore}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, + circuits::AssignmentsOwned, + commitments::GroupBasedCommitment, + traits::SonobeField, + transcripts::Transcript, +}; + +use crate::{ + nova::{AbstractNova, AbstractNova2, NovaKey}, + Error, FoldingSchemeProver, +}; + +impl + FoldingSchemeProver<1, 1> for AbstractNova +{ + #[allow(non_snake_case)] + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Compute the cross term `T` by following the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u.x).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w.w).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(( + Self::RW { + e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), + r_e: W.r_e + r_t * rho, + w: cfg_iter!(W.w).zip(&w.w).map(|(a, b)| rho * b + a).collect(), + r_w: W.r_w + w.r_w * rho, + }, + Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + u.cm_w.mul(rho), + x: cfg_iter!(U.x).zip(&u.x).map(|(a, b)| rho * b + a).collect(), + }, + cm_t, + rho_bits.try_into().unwrap(), + )) + } +} + +impl + FoldingSchemeProver<2, 0> for AbstractNova +{ + #[allow(non_snake_case)] + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + [W1, W2]: &[impl Borrow; 2], + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + _: &[impl Borrow; 0], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<2, 0>, Self::Challenge), Error> { + let (W1, U1) = (W1.borrow(), U1.borrow()); + let (W2, U2) = (W2.borrow(), U2.borrow()); + + // Compute the cross term `T` by following the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U1.u + U2.u, + cfg_iter!(U1.x).zip(&U2.x).map(|(a, b)| *a + b).collect(), + cfg_iter!(W1.w).zip(&W2.w).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W1.e) + .zip(&W2.e) + .map(|((a, b), c)| a - b - c) + .collect::>(); + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; + + let rho_bits = { + transcript.add(&U1); + transcript.add(&U2); + transcript.add(&cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + let rho_squared = rho * rho; + + Ok(( + Self::RW { + e: cfg_iter!(W1.e) + .zip(&t) + .zip(&W2.e) + .map(|((a, b), c)| rho_squared * c + rho * b + a) + .collect(), + r_e: W1.r_e + r_t * rho + W2.r_e * rho_squared, + w: cfg_iter!(W1.w) + .zip(&W2.w) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W1.r_w + W2.r_w * rho, + }, + Self::RU { + cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), + u: U1.u + rho * U2.u, + cm_w: U1.cm_w + U2.cm_w.mul(rho), + x: cfg_iter!(U1.x) + .zip(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }, + cm_t, + rho_bits.try_into().unwrap(), + )) + } +} + +impl + FoldingSchemeProver<1, 1> for AbstractNova2 +{ + #[allow(non_snake_case)] + fn prove( + pk: &NovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + mut rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Compute the cross term `T` by following the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u[..]).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w[..]).map(|(a, b)| *a + b).collect(), + )))?; + let t = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + + let (cm_w, r_w) = CM::commit(&pk.ck, w, &mut rng)?; + + let (cm_t, r_t) = CM::commit(&pk.ck, &t, &mut rng)?; + + let pi = (cm_w, cm_t); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&pi); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(( + Self::RW { + e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), + r_e: W.r_e + r_t * rho, + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W.r_w + r_w * rho, + }, + Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + pi, + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/nova/algorithms/verifier.rs b/crates/fs/src/nova/algorithms/verifier.rs new file mode 100644 index 000000000..30dc056fe --- /dev/null +++ b/crates/fs/src/nova/algorithms/verifier.rs @@ -0,0 +1,111 @@ +use ark_std::{borrow::Borrow, cfg_iter, ops::Mul}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, commitments::GroupBasedCommitment, traits::SonobeField, + transcripts::Transcript, +}; + +use crate::{ + nova::{AbstractNova, AbstractNova2}, + Error, FoldingSchemeVerifier, +}; + +impl + FoldingSchemeVerifier<1, 1> for AbstractNova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + cm_t: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + u.cm_w.mul(rho), + x: cfg_iter!(U.x).zip(&u.x).map(|(a, b)| rho * b + a).collect(), + }) + } +} + +impl + FoldingSchemeVerifier<2, 0> for AbstractNova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + [U1, U2]: &[impl Borrow; 2], + _: &[impl Borrow; 0], + cm_t: &Self::Proof<2, 0>, + ) -> Result { + let (U1, U2) = (U1.borrow(), U2.borrow()); + + let rho_bits = { + transcript.add(&U1); + transcript.add(&U2); + transcript.add(cm_t); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + let rho_squared = rho * rho; + + Ok(Self::RU { + cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), + u: U1.u + rho * U2.u, + cm_w: U1.cm_w + U2.cm_w.mul(rho), + x: cfg_iter!(U1.x) + .zip(&U2.x) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} + +impl + FoldingSchemeVerifier<1, 1> for AbstractNova2 +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + pi: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(pi); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + let (cm_w, cm_t) = pi; + + Ok(Self::RU { + cm_e: U.cm_e + cm_t.mul(rho), + u: U.u + rho, + cm_w: U.cm_w + cm_w.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} diff --git a/crates/fs/src/nova/circuits/mod.rs b/crates/fs/src/nova/circuits/mod.rs new file mode 100644 index 000000000..9a0722027 --- /dev/null +++ b/crates/fs/src/nova/circuits/mod.rs @@ -0,0 +1 @@ +pub mod verifier; diff --git a/crates/fs/src/nova/circuits/verifier.rs b/crates/fs/src/nova/circuits/verifier.rs new file mode 100644 index 000000000..b550373be --- /dev/null +++ b/crates/fs/src/nova/circuits/verifier.rs @@ -0,0 +1,158 @@ +use ark_ff::{One, Zero}; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, groups::CurveVar}; +use ark_relations::gr1cs::SynthesisError; +use num_bigint::BigInt; +use sonobe_primitives::{ + algebra::{field::emulated::Bounds, ops::bits::FromBitsGadget}, + commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, + transcripts::TranscriptGadget, +}; + +use crate::{ + FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, nova::AbstractNovaGadget, +}; + +impl FoldingSchemePartialVerifierGadget<1, 1> + for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: CM::CommitmentVar::new_witness( + U.cm_e.cs().or(proof.cs()).or(rho.cs()), + || { + Ok(U.cm_e.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) + }, + )?, + cm_w: CM::CommitmentVar::new_witness( + U.cm_w.cs().or(u.cm_w.cs()).or(rho.cs()), + || { + Ok(U.cm_w.value().unwrap_or_default() + + u.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + }, + )?, + x: U.x + .iter() + .zip(&u.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemePartialVerifierGadget<2, 0> + for AbstractNovaGadget +where + CM: CommitmentDefGadget, +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U1, U2]: [&Self::RU; 2], + _: [&Self::IU; 0], + proof: &Self::Proof<2, 0>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U1)?; + transcript.add(&U2)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + u: (U2.u.clone() * &rho + &U1.u) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: CM::CommitmentVar::new_witness( + U1.cm_e.cs().or(U2.cm_e.cs()).or(proof.cs()).or(rho.cs()), + || { + let rho = rho.value().unwrap_or_default(); + Ok(U1.cm_e.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho + + U2.cm_e.value().unwrap_or_default() * rho * rho) + }, + )?, + cm_w: CM::CommitmentVar::new_witness( + U1.cm_w.cs().or(U2.cm_w.cs()).or(rho.cs()), + || { + Ok(U1.cm_w.value().unwrap_or_default() + + U2.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + }, + )?, + x: U1 + .x + .iter() + .zip(&U2.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeFullVerifierGadget<1, 1> + for AbstractNovaGadget +where + CM: CommitmentDefGadget, + CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, +{ + #[allow(non_snake_case)] + fn verify( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm_e: proof.scalar_mul_le(rho_bits.iter())? + &U.cm_e, + cm_w: u.cm_w.scalar_mul_le(rho_bits.iter())? + &U.cm_w, + x: U.x + .iter() + .zip(&u.x) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} diff --git a/crates/fs/src/nova/instance/circuits.rs b/crates/fs/src/nova/instances/circuits.rs similarity index 100% rename from crates/fs/src/nova/instance/circuits.rs rename to crates/fs/src/nova/instances/circuits.rs diff --git a/crates/fs/src/nova/instance/mod.rs b/crates/fs/src/nova/instances/mod.rs similarity index 100% rename from crates/fs/src/nova/instance/mod.rs rename to crates/fs/src/nova/instances/mod.rs diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 4dd3dd09f..22fd55d6a 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -1,43 +1,32 @@ -use ark_ff::One; -use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, groups::CurveVar}; -use ark_relations::gr1cs::SynthesisError; -use ark_std::{ - UniformRand, borrow::Borrow, cfg_into_iter, cfg_iter, marker::PhantomData, ops::Mul, - rand::RngCore, sync::Arc, -}; -#[cfg(feature = "parallel")] -use rayon::prelude::*; +use ark_r1cs_std::boolean::Boolean; +use ark_std::{UniformRand, marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_primitives::{ - algebra::ops::bits::{FromBits, FromBitsGadget}, arithmetizations::{ Arith, ArithConfig, ArithRelation, r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, }, circuits::AssignmentsOwned, - commitments::{ - CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, GroupBasedCommitment, - }, + commitments::{CommitmentDef, CommitmentDefGadget, CommitmentOps, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, traits::{CF2, SonobeField}, - transcripts::{Transcript, TranscriptGadget}, }; use self::{ - instance::{ + instances::{ IncomingInstance as IU, RunningInstance as RU, circuits::{IncomingInstanceVar as IUVar, RunningInstanceVar as RUVar}, }, - witness::{IncomingWitness as IW, RunningWitness as RW}, + witnesses::{IncomingWitness as IW, RunningWitness as RW}, }; use crate::{ - DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, - FoldingSchemeKeyGenerator, FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, - FoldingSchemeProver, FoldingSchemeVerifier, GroupBasedFoldingSchemePrimaryDef, + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, GroupBasedFoldingSchemePrimaryDef, GroupBasedFoldingSchemeSecondaryDef, PlainInstance as PU, PlainWitness as PW, }; -pub mod instance; -pub mod witness; +pub mod algorithms; +pub mod circuits; +pub mod instances; +pub mod witnesses; #[derive(Clone)] pub struct NovaKey { @@ -200,221 +189,6 @@ impl Fol type Proof = CM::Commitment; } -impl - FoldingSchemePreprocessor for AbstractNova -{ - fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { - let ck = CM::generate_key(ck_len, &mut rng)?; - Ok(ck) - } -} - -impl - FoldingSchemeKeyGenerator for AbstractNova -{ - fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { - let ck = Arc::new(ck); - let r1cs = Arc::new(r1cs); - let cfg = r1cs.config(); - if ck.max_scalars_len() < cfg.n_constraints().max(cfg.n_witnesses()) { - return Err(Error::InvalidPublicParameters( - "The commitment key is too short for the R1CS instance".into(), - )); - } - Ok(Self::DeciderKey { arith: r1cs, ck }) - } -} - -impl - FoldingSchemeProver<1, 1> for AbstractNova -{ - #[allow(non_snake_case)] - fn prove( - pk: &NovaKey, - transcript: &mut impl Transcript, - Ws: &[impl Borrow; 1], - Us: &[impl Borrow; 1], - ws: &[impl Borrow; 1], - us: &[impl Borrow; 1], - rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { - let (W, U) = (Ws[0].borrow(), Us[0].borrow()); - let (w, u) = (ws[0].borrow(), us[0].borrow()); - - // Compute the cross term `T` by following the optimized approach in - // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. - let v = pk.arith.evaluate_at(AssignmentsOwned::from(( - U.u + CM::Scalar::one(), - cfg_iter!(U.x).zip(&u.x).map(|(a, b)| *a + b).collect(), - cfg_iter!(W.w).zip(&w.w).map(|(a, b)| *a + b).collect(), - )))?; - let t = cfg_into_iter!(v) - .zip(&W.e) - .map(|(a, b)| a - b) - .collect::>(); - - let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; - - let rho_bits = { - transcript.add(&U); - transcript.add(&u); - transcript.add(&cm_t); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - - Ok(( - RW { - e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), - r_e: W.r_e + r_t * rho, - w: cfg_iter!(W.w).zip(&w.w).map(|(a, b)| rho * b + a).collect(), - r_w: W.r_w + w.r_w * rho, - }, - RU { - cm_e: U.cm_e + cm_t.mul(rho), - u: U.u + rho, - cm_w: U.cm_w + u.cm_w.mul(rho), - x: cfg_iter!(U.x).zip(&u.x).map(|(a, b)| rho * b + a).collect(), - }, - cm_t, - rho_bits.try_into().unwrap(), - )) - } -} - -impl - FoldingSchemeVerifier<1, 1> for AbstractNova -{ - #[allow(non_snake_case)] - fn verify( - _vk: &(), - transcript: &mut impl Transcript, - Us: &[impl Borrow; 1], - us: &[impl Borrow; 1], - cm_t: &Self::Proof<1, 1>, - ) -> Result { - let (U, u) = (Us[0].borrow(), us[0].borrow()); - - let rho_bits = { - transcript.add(&U); - transcript.add(&u); - transcript.add(&cm_t); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - - Ok(RU { - cm_e: U.cm_e + cm_t.mul(rho), - u: U.u + rho, - cm_w: U.cm_w + u.cm_w.mul(rho), - x: cfg_iter!(U.x).zip(&u.x).map(|(a, b)| rho * b + a).collect(), - }) - } -} - -impl - FoldingSchemeProver<2, 0> for AbstractNova -{ - #[allow(non_snake_case)] - fn prove( - pk: &NovaKey, - transcript: &mut impl Transcript, - [W1, W2]: &[impl Borrow; 2], - [U1, U2]: &[impl Borrow; 2], - _: &[impl Borrow; 0], - _: &[impl Borrow; 0], - rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof<2, 0>, Self::Challenge), Error> { - let (W1, U1) = (W1.borrow(), U1.borrow()); - let (W2, U2) = (W2.borrow(), U2.borrow()); - - // Compute the cross term `T` by following the optimized approach in - // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. - let v = pk.arith.evaluate_at(AssignmentsOwned::from(( - U1.u + U2.u, - cfg_iter!(U1.x).zip(&U2.x).map(|(a, b)| *a + b).collect(), - cfg_iter!(W1.w).zip(&W2.w).map(|(a, b)| *a + b).collect(), - )))?; - let t = cfg_into_iter!(v) - .zip(&W1.e) - .zip(&W2.e) - .map(|((a, b), c)| a - b - c) - .collect::>(); - - let (cm_t, r_t) = CM::commit(&pk.ck, &t, rng)?; - - let rho_bits = { - transcript.add(&U1); - transcript.add(&U2); - transcript.add(&cm_t); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - let rho_squared = rho * rho; - - Ok(( - RW { - e: cfg_iter!(W1.e) - .zip(&t) - .zip(&W2.e) - .map(|((a, b), c)| rho_squared * c + rho * b + a) - .collect(), - r_e: W1.r_e + r_t * rho + W2.r_e * rho_squared, - w: cfg_iter!(W1.w) - .zip(&W2.w) - .map(|(a, b)| rho * b + a) - .collect(), - r_w: W1.r_w + W2.r_w * rho, - }, - RU { - cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), - u: U1.u + rho * U2.u, - cm_w: U1.cm_w + U2.cm_w.mul(rho), - x: cfg_iter!(U1.x) - .zip(&U2.x) - .map(|(a, b)| rho * b + a) - .collect(), - }, - cm_t, - rho_bits.try_into().unwrap(), - )) - } -} - -impl - FoldingSchemeVerifier<2, 0> for AbstractNova -{ - #[allow(non_snake_case)] - fn verify( - _vk: &(), - transcript: &mut impl Transcript, - [U1, U2]: &[impl Borrow; 2], - _: &[impl Borrow; 0], - cm_t: &Self::Proof<2, 0>, - ) -> Result { - let (U1, U2) = (U1.borrow(), U2.borrow()); - - let rho_bits = { - transcript.add(&U1); - transcript.add(&U2); - transcript.add(cm_t); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - let rho_squared = rho * rho; - - Ok(RU { - cm_e: U1.cm_e + cm_t.mul(rho) + U2.cm_e.mul(rho_squared), - u: U1.u + rho * U2.u, - cm_w: U1.cm_w + U2.cm_w.mul(rho), - x: cfg_iter!(U1.x) - .zip(&U2.x) - .map(|(a, b)| rho * b + a) - .collect(), - }) - } -} - // used for the RO challenges. // From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness // error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this @@ -449,133 +223,6 @@ impl Fol type Proof = (CM::Commitment, CM::Commitment); } -impl - FoldingSchemePreprocessor for AbstractNova2 -{ - fn preprocess(ck_len: usize, mut rng: impl RngCore) -> Result { - let ck = CM::generate_key(ck_len, &mut rng)?; - Ok(ck) - } -} - -impl - FoldingSchemeKeyGenerator for AbstractNova2 -{ - fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { - let ck = Arc::new(ck); - let r1cs = Arc::new(r1cs); - let cfg = r1cs.config(); - if ck.max_scalars_len() < cfg.n_constraints().max(cfg.n_witnesses()) { - return Err(Error::InvalidPublicParameters( - "The commitment key is too short for the R1CS instance".into(), - )); - } - Ok(Self::DeciderKey { arith: r1cs, ck }) - } -} - -impl - FoldingSchemeProver<1, 1> for AbstractNova2 -{ - #[allow(non_snake_case)] - fn prove( - pk: &NovaKey, - transcript: &mut impl Transcript, - Ws: &[impl Borrow; 1], - Us: &[impl Borrow; 1], - ws: &[impl Borrow; 1], - us: &[impl Borrow; 1], - mut rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { - let (W, U) = (Ws[0].borrow(), Us[0].borrow()); - let (w, u) = (ws[0].borrow(), us[0].borrow()); - - // Compute the cross term `T` by following the optimized approach in - // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. - let v = pk.arith.evaluate_at(AssignmentsOwned::from(( - U.u + CM::Scalar::one(), - cfg_iter!(U.x).zip(&u[..]).map(|(a, b)| *a + b).collect(), - cfg_iter!(W.w).zip(&w[..]).map(|(a, b)| *a + b).collect(), - )))?; - let t = cfg_into_iter!(v) - .zip(&W.e) - .map(|(a, b)| a - b) - .collect::>(); - - let (cm_w, r_w) = CM::commit(&pk.ck, w, &mut rng)?; - - let (cm_t, r_t) = CM::commit(&pk.ck, &t, &mut rng)?; - - let pi = (cm_w, cm_t); - - let rho_bits = { - transcript.add(&U); - transcript.add(&u); - transcript.add(&pi); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - - Ok(( - RW { - e: cfg_iter!(W.e).zip(&t).map(|(a, b)| rho * b + a).collect(), - r_e: W.r_e + r_t * rho, - w: cfg_iter!(W.w) - .zip(&w[..]) - .map(|(a, b)| rho * b + a) - .collect(), - r_w: W.r_w + r_w * rho, - }, - RU { - cm_e: U.cm_e + cm_t.mul(rho), - u: U.u + rho, - cm_w: U.cm_w + cm_w.mul(rho), - x: cfg_iter!(U.x) - .zip(&u[..]) - .map(|(a, b)| rho * b + a) - .collect(), - }, - pi, - rho_bits.try_into().unwrap(), - )) - } -} - -impl - FoldingSchemeVerifier<1, 1> for AbstractNova2 -{ - #[allow(non_snake_case)] - fn verify( - _vk: &(), - transcript: &mut impl Transcript, - Us: &[impl Borrow; 1], - us: &[impl Borrow; 1], - pi: &Self::Proof<1, 1>, - ) -> Result { - let (U, u) = (Us[0].borrow(), us[0].borrow()); - - let rho_bits = { - transcript.add(&U); - transcript.add(&u); - transcript.add(pi); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - - let (cm_w, cm_t) = pi; - - Ok(RU { - cm_e: U.cm_e + cm_t.mul(rho), - u: U.u + rho, - cm_w: U.cm_w + cm_w.mul(rho), - x: cfg_iter!(U.x) - .zip(&u[..]) - .map(|(a, b)| rho * b + a) - .collect(), - }) - } -} - pub struct AbstractNovaGadget { _vc: PhantomData, } @@ -595,151 +242,6 @@ where type Proof = CM::CommitmentVar; } -impl FoldingSchemePartialVerifierGadget<1, 1> - for AbstractNovaGadget -where - CM: CommitmentDefGadget, -{ - #[allow(non_snake_case)] - fn verify_hinted( - _vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - [U]: [&Self::RU; 1], - [u]: [&Self::IU; 1], - proof: &Self::Proof<1, 1>, - ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { - let rho_bits = { - transcript.add(&U)?; - transcript.add(&u)?; - transcript.add(proof)?; - transcript.challenge_bits(CHALLENGE_BITS)? - }; - let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; - - Ok(( - Self::RU { - u: (U.u.clone() + &rho) - .try_into() - .map_err(|_| SynthesisError::Unsatisfiable)?, - cm_e: CM::CommitmentVar::new_witness( - U.cm_e.cs().or(proof.cs()).or(rho.cs()), - || { - Ok(U.cm_e.value().unwrap_or_default() - + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) - }, - )?, - cm_w: CM::CommitmentVar::new_witness( - U.cm_w.cs().or(u.cm_w.cs()).or(rho.cs()), - || { - Ok(U.cm_w.value().unwrap_or_default() - + u.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) - }, - )?, - x: U.x - .iter() - .zip(&u.x) - .map(|(a, b)| (b.clone() * &rho + a).try_into()) - .collect::>() - .map_err(|_| SynthesisError::Unsatisfiable)?, - }, - rho_bits.try_into().unwrap(), - )) - } -} - -impl FoldingSchemePartialVerifierGadget<2, 0> - for AbstractNovaGadget -where - CM: CommitmentDefGadget, -{ - #[allow(non_snake_case)] - fn verify_hinted( - _vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - [U1, U2]: [&Self::RU; 2], - _: [&Self::IU; 0], - proof: &Self::Proof<2, 0>, - ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { - let rho_bits = { - transcript.add(&U1)?; - transcript.add(&U2)?; - transcript.add(proof)?; - transcript.challenge_bits(CHALLENGE_BITS)? - }; - let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; - - Ok(( - Self::RU { - u: (U2.u.clone() * &rho + &U1.u) - .try_into() - .map_err(|_| SynthesisError::Unsatisfiable)?, - cm_e: CM::CommitmentVar::new_witness( - U1.cm_e.cs().or(U2.cm_e.cs()).or(proof.cs()).or(rho.cs()), - || { - let rho = rho.value().unwrap_or_default(); - Ok(U1.cm_e.value().unwrap_or_default() - + proof.value().unwrap_or_default() * rho - + U2.cm_e.value().unwrap_or_default() * rho * rho) - }, - )?, - cm_w: CM::CommitmentVar::new_witness( - U1.cm_w.cs().or(U2.cm_w.cs()).or(rho.cs()), - || { - Ok(U1.cm_w.value().unwrap_or_default() - + U2.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) - }, - )?, - x: U1 - .x - .iter() - .zip(&U2.x) - .map(|(a, b)| (b.clone() * &rho + a).try_into()) - .collect::>() - .map_err(|_| SynthesisError::Unsatisfiable)?, - }, - rho_bits.try_into().unwrap(), - )) - } -} - -impl FoldingSchemeFullVerifierGadget<1, 1> - for AbstractNovaGadget -where - CM: CommitmentDefGadget, - CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, -{ - #[allow(non_snake_case)] - fn verify( - _vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - [U]: [&Self::RU; 1], - [u]: [&Self::IU; 1], - proof: &Self::Proof<1, 1>, - ) -> Result { - let rho_bits = { - transcript.add(&U)?; - transcript.add(&u)?; - transcript.add(proof)?; - transcript.challenge_bits(CHALLENGE_BITS)? - }; - let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; - - Ok(Self::RU { - u: (U.u.clone() + &rho) - .try_into() - .map_err(|_| SynthesisError::Unsatisfiable)?, - cm_e: proof.scalar_mul_le(rho_bits.iter())? + &U.cm_e, - cm_w: u.cm_w.scalar_mul_le(rho_bits.iter())? + &U.cm_w, - x: U.x - .iter() - .zip(&u.x) - .map(|(a, b)| (b.clone() * &rho + a).try_into()) - .collect::>() - .map_err(|_| SynthesisError::Unsatisfiable)?, - }) - } -} - impl GroupBasedFoldingSchemePrimaryDef for AbstractNova { diff --git a/crates/fs/src/nova/witness/circuits.rs b/crates/fs/src/nova/witnesses/circuits.rs similarity index 100% rename from crates/fs/src/nova/witness/circuits.rs rename to crates/fs/src/nova/witnesses/circuits.rs diff --git a/crates/fs/src/nova/witness/mod.rs b/crates/fs/src/nova/witnesses/mod.rs similarity index 100% rename from crates/fs/src/nova/witness/mod.rs rename to crates/fs/src/nova/witnesses/mod.rs From 796ed35b8c836273b79385f21c50dd9e6cadf792 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 01:57:52 +0800 Subject: [PATCH 78/93] Clean up --- crates/fs/src/nova/algorithms/mod.rs | 2 +- crates/fs/src/nova/algorithms/preprocessor.rs | 2 +- crates/fs/src/nova/algorithms/prover.rs | 9 +++------ crates/fs/src/nova/algorithms/verifier.rs | 2 +- crates/fs/src/nova/mod.rs | 6 +++--- crates/ivc/src/compilers/cyclefold/adapters/nova.rs | 4 ++++ 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/fs/src/nova/algorithms/mod.rs b/crates/fs/src/nova/algorithms/mod.rs index b0960535b..11e838298 100644 --- a/crates/fs/src/nova/algorithms/mod.rs +++ b/crates/fs/src/nova/algorithms/mod.rs @@ -1,4 +1,4 @@ -pub mod preprocessor; pub mod key_generator; +pub mod preprocessor; pub mod prover; pub mod verifier; diff --git a/crates/fs/src/nova/algorithms/preprocessor.rs b/crates/fs/src/nova/algorithms/preprocessor.rs index 916aa08bd..c41dbce6b 100644 --- a/crates/fs/src/nova/algorithms/preprocessor.rs +++ b/crates/fs/src/nova/algorithms/preprocessor.rs @@ -2,8 +2,8 @@ use ark_std::rand::RngCore; use sonobe_primitives::{commitments::GroupBasedCommitment, traits::SonobeField}; use crate::{ - nova::{AbstractNova, AbstractNova2}, Error, FoldingSchemePreprocessor, + nova::{AbstractNova, AbstractNova2}, }; impl diff --git a/crates/fs/src/nova/algorithms/prover.rs b/crates/fs/src/nova/algorithms/prover.rs index 3e163b878..49901a0d4 100644 --- a/crates/fs/src/nova/algorithms/prover.rs +++ b/crates/fs/src/nova/algorithms/prover.rs @@ -3,16 +3,13 @@ use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, ops::Mul, rand::RngCore}; #[cfg(feature = "parallel")] use rayon::prelude::*; use sonobe_primitives::{ - algebra::ops::bits::FromBits, - circuits::AssignmentsOwned, - commitments::GroupBasedCommitment, - traits::SonobeField, - transcripts::Transcript, + algebra::ops::bits::FromBits, circuits::AssignmentsOwned, commitments::GroupBasedCommitment, + traits::SonobeField, transcripts::Transcript, }; use crate::{ - nova::{AbstractNova, AbstractNova2, NovaKey}, Error, FoldingSchemeProver, + nova::{AbstractNova, AbstractNova2, NovaKey}, }; impl diff --git a/crates/fs/src/nova/algorithms/verifier.rs b/crates/fs/src/nova/algorithms/verifier.rs index 30dc056fe..e682e5b36 100644 --- a/crates/fs/src/nova/algorithms/verifier.rs +++ b/crates/fs/src/nova/algorithms/verifier.rs @@ -7,8 +7,8 @@ use sonobe_primitives::{ }; use crate::{ - nova::{AbstractNova, AbstractNova2}, Error, FoldingSchemeVerifier, + nova::{AbstractNova, AbstractNova2}, }; impl diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 22fd55d6a..01e5c7d43 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -194,14 +194,14 @@ impl Fol // error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this // case, we keep the size of S close to 2^128. // TODO: experimental design -struct AbstractNova2 { +pub struct AbstractNova2 { _t: PhantomData<(CM, TF)>, } -type Nova2 = +pub type Nova2 = AbstractNova2::Scalar, CHALLENGE_BITS>; -type CycleFoldNova2 = +pub type CycleFoldNova2 = AbstractNova2::Commitment>, CHALLENGE_BITS>; impl FoldingSchemeDef diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs index c3cd4ae35..c694b0942 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -62,6 +62,7 @@ impl FoldingSchemeCycleFo type CFCircuit = NovaCycleFoldCircuit; + #[allow(non_snake_case)] fn to_cyclefold_circuits( [U]: &[impl Borrow; 1], [u]: &[impl Borrow; 1], @@ -80,6 +81,7 @@ impl FoldingSchemeCycleFo ] } + #[allow(non_snake_case)] fn to_cyclefold_inputs( [U]: [::RU; 1], [u]: [::IU; 1], @@ -122,6 +124,7 @@ impl FoldingSchemeCycleFo type CFCircuit = NovaCycleFoldCircuit; + #[allow(non_snake_case)] fn to_cyclefold_circuits( [U1, U2]: &[impl Borrow; 2], _: &[impl Borrow; 0], @@ -145,6 +148,7 @@ impl FoldingSchemeCycleFo ] } + #[allow(non_snake_case)] fn to_cyclefold_inputs( [U1, U2]: [::RU; 2], _: [::IU; 0], From ec71a50189db1224e576cd92d9f74d77f8b8a60b Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 18:35:53 +0800 Subject: [PATCH 79/93] Actually test wasm targets --- crates/fs/src/nova/mod.rs | 6 ++++-- crates/ivc/src/compilers/cyclefold/adapters/nova.rs | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 01e5c7d43..12e3e2240 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -258,11 +258,13 @@ impl GroupBasedFoldingSch mod tests { use ark_bn254::{Fq, Fr, G1Projective}; use ark_ff::UniformRand; - use ark_std::{error::Error, test_rng}; + use ark_std::{error::Error, rand::thread_rng}; use sonobe_primitives::{ circuits::utils::{CircuitForTest, satisfying_assignments_for_test}, commitments::pedersen::Pedersen, }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::tests::test_folding_scheme; @@ -341,7 +343,7 @@ mod tests { #[test] fn test_nova() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); test_nova_opt::(10, &mut rng)?; test_nova_opt::(10, &mut rng)?; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs index c694b0942..88eab910a 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -207,6 +207,8 @@ mod tests { commitments::pedersen::Pedersen, transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::tests::test_ivc; From 0e51dcac5ffab9f00e4fb9602feb98d981fa6eb7 Mon Sep 17 00:00:00 2001 From: winderica Date: Mon, 9 Feb 2026 19:36:40 +0800 Subject: [PATCH 80/93] Add FS & IVC docs --- .../fs/src/nova/algorithms/key_generator.rs | 2 ++ crates/fs/src/nova/algorithms/mod.rs | 2 ++ crates/fs/src/nova/algorithms/preprocessor.rs | 2 ++ crates/fs/src/nova/algorithms/prover.rs | 2 ++ crates/fs/src/nova/algorithms/verifier.rs | 2 ++ crates/fs/src/nova/circuits/mod.rs | 2 ++ crates/fs/src/nova/circuits/verifier.rs | 6 ++--- crates/fs/src/nova/instances/circuits.rs | 12 ++++++++++ crates/fs/src/nova/instances/mod.rs | 11 ++++++++++ crates/fs/src/nova/mod.rs | 22 ++++++++++++++++++- crates/fs/src/nova/witnesses/circuits.rs | 12 ++++++++++ crates/fs/src/nova/witnesses/mod.rs | 11 ++++++++++ .../src/compilers/cyclefold/adapters/nova.rs | 6 ++++- 13 files changed, 87 insertions(+), 5 deletions(-) diff --git a/crates/fs/src/nova/algorithms/key_generator.rs b/crates/fs/src/nova/algorithms/key_generator.rs index 7e49489ba..89104470e 100644 --- a/crates/fs/src/nova/algorithms/key_generator.rs +++ b/crates/fs/src/nova/algorithms/key_generator.rs @@ -1,3 +1,5 @@ +//! Key generation for Nova. + use ark_std::sync::Arc; use sonobe_primitives::{ arithmetizations::{Arith, ArithConfig}, diff --git a/crates/fs/src/nova/algorithms/mod.rs b/crates/fs/src/nova/algorithms/mod.rs index 11e838298..263d3cd42 100644 --- a/crates/fs/src/nova/algorithms/mod.rs +++ b/crates/fs/src/nova/algorithms/mod.rs @@ -1,3 +1,5 @@ +//! Implementations folding scheme algorithms for Nova. + pub mod key_generator; pub mod preprocessor; pub mod prover; diff --git a/crates/fs/src/nova/algorithms/preprocessor.rs b/crates/fs/src/nova/algorithms/preprocessor.rs index c41dbce6b..316fbbec6 100644 --- a/crates/fs/src/nova/algorithms/preprocessor.rs +++ b/crates/fs/src/nova/algorithms/preprocessor.rs @@ -1,3 +1,5 @@ +//! Preprocessing for Nova. + use ark_std::rand::RngCore; use sonobe_primitives::{commitments::GroupBasedCommitment, traits::SonobeField}; diff --git a/crates/fs/src/nova/algorithms/prover.rs b/crates/fs/src/nova/algorithms/prover.rs index 49901a0d4..de6ec8485 100644 --- a/crates/fs/src/nova/algorithms/prover.rs +++ b/crates/fs/src/nova/algorithms/prover.rs @@ -1,3 +1,5 @@ +//! Proof generation for Nova. + use ark_ff::One; use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, ops::Mul, rand::RngCore}; #[cfg(feature = "parallel")] diff --git a/crates/fs/src/nova/algorithms/verifier.rs b/crates/fs/src/nova/algorithms/verifier.rs index e682e5b36..bfc3f89b9 100644 --- a/crates/fs/src/nova/algorithms/verifier.rs +++ b/crates/fs/src/nova/algorithms/verifier.rs @@ -1,3 +1,5 @@ +//! Proof verification for Nova. + use ark_std::{borrow::Borrow, cfg_iter, ops::Mul}; #[cfg(feature = "parallel")] use rayon::prelude::*; diff --git a/crates/fs/src/nova/circuits/mod.rs b/crates/fs/src/nova/circuits/mod.rs index 9a0722027..8d54a8eb9 100644 --- a/crates/fs/src/nova/circuits/mod.rs +++ b/crates/fs/src/nova/circuits/mod.rs @@ -1 +1,3 @@ +//! In-circuit gadgets for Nova. + pub mod verifier; diff --git a/crates/fs/src/nova/circuits/verifier.rs b/crates/fs/src/nova/circuits/verifier.rs index b550373be..a69bb0835 100644 --- a/crates/fs/src/nova/circuits/verifier.rs +++ b/crates/fs/src/nova/circuits/verifier.rs @@ -1,9 +1,9 @@ -use ark_ff::{One, Zero}; +//! Partial and full in-circuit verifier implementations for Nova. + use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, groups::CurveVar}; use ark_relations::gr1cs::SynthesisError; -use num_bigint::BigInt; use sonobe_primitives::{ - algebra::{field::emulated::Bounds, ops::bits::FromBitsGadget}, + algebra::ops::bits::FromBitsGadget, commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, transcripts::TranscriptGadget, }; diff --git a/crates/fs/src/nova/instances/circuits.rs b/crates/fs/src/nova/instances/circuits.rs index 9a850957c..6d0c81007 100644 --- a/crates/fs/src/nova/instances/circuits.rs +++ b/crates/fs/src/nova/instances/circuits.rs @@ -1,3 +1,5 @@ +//! In-circuit variables for Nova instances. + use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, @@ -12,11 +14,17 @@ use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::Absorbabl use super::{IncomingInstance, RunningInstance}; use crate::FoldingInstanceVar; +/// [`RunningInstanceVar`] defines Nova's running instance variable. #[derive(Clone, Debug, PartialEq)] pub struct RunningInstanceVar { + /// [`RunningInstanceVar::cm_e`] is the error term commitment. pub cm_e: CM::CommitmentVar, + /// [`RunningInstanceVar::u`] is the constant term. pub u: CM::ScalarVar, + /// [`RunningInstanceVar::cm_w`] is the witness commitment. pub cm_w: CM::CommitmentVar, + /// [`RunningInstanceVar::x`] is the vector of public inputs (to the + /// circuit). pub x: Vec, } @@ -120,9 +128,13 @@ impl FoldingInstanceVar for RunningInstanceVar } } +/// [`IncomingInstanceVar`] defines Nova's incoming instance variable. #[derive(Clone, Debug, PartialEq)] pub struct IncomingInstanceVar { + /// [`IncomingInstanceVar::cm_w`] is the witness commitment. pub cm_w: CM::CommitmentVar, + /// [`IncomingInstanceVar::x`] is the vector of public inputs (to the + /// circuit). pub x: Vec, } diff --git a/crates/fs/src/nova/instances/mod.rs b/crates/fs/src/nova/instances/mod.rs index ba81b5838..a2408bafb 100644 --- a/crates/fs/src/nova/instances/mod.rs +++ b/crates/fs/src/nova/instances/mod.rs @@ -1,3 +1,6 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Nova +//! instances. + use ark_ff::PrimeField; use sonobe_primitives::{ arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, @@ -8,11 +11,16 @@ use crate::FoldingInstance; pub mod circuits; +/// [`RunningInstance`] defines Nova's running instance. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningInstance { + /// [`RunningInstance::cm_e`] is the error term commitment. pub cm_e: CM::Commitment, + /// [`RunningInstance::u`] is the constant term. pub u: CM::Scalar, + /// [`RunningInstance::cm_w`] is the witness commitment. pub cm_w: CM::Commitment, + /// [`RunningInstance::x`] is the vector of public inputs (to the circuit). pub x: Vec, } @@ -52,9 +60,12 @@ impl Absorbable for RunningInstance { } } +/// [`IncomingInstance`] defines Nova's incoming instance. #[derive(Clone, Debug, Eq, PartialEq)] pub struct IncomingInstance { + /// [`IncomingInstance::cm_w`] is the witness commitment. pub cm_w: CM::Commitment, + /// [`IncomingInstance::x`] is the vector of public inputs (to the circuit). pub x: Vec, } diff --git a/crates/fs/src/nova/mod.rs b/crates/fs/src/nova/mod.rs index 12e3e2240..e9988cf2d 100644 --- a/crates/fs/src/nova/mod.rs +++ b/crates/fs/src/nova/mod.rs @@ -1,3 +1,8 @@ +//! This module implements the Nova folding scheme, which is introduced in this +//! [paper]. +//! +//! [paper]: https://eprint.iacr.org/2021/370.pdf + use ark_r1cs_std::boolean::Boolean; use ark_std::{UniformRand, marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_primitives::{ @@ -28,6 +33,7 @@ pub mod circuits; pub mod instances; pub mod witnesses; +/// [`NovaKey`] is Nova's decider key. #[derive(Clone)] pub struct NovaKey { arith: Arc, @@ -160,13 +166,18 @@ where // From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness // error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this // case, we keep the size of S close to 2^128. +/// [`AbstractNova`] implements the Nova folding scheme which can operate on +/// both the primary and secondary curves. pub struct AbstractNova { _t: PhantomData<(CM, TF)>, } +/// [`Nova`] is the main Nova folding scheme on the primary curve. pub type Nova = AbstractNova::Scalar, CHALLENGE_BITS>; +/// [`CycleFoldNova`] is the Nova folding scheme on the secondary curve which +/// can be used as the folding scheme for folding CycleFold instances. pub type CycleFoldNova = AbstractNova::Commitment>, CHALLENGE_BITS>; @@ -193,14 +204,22 @@ impl Fol // From [Srinath Setty](https://microsoft.com/en-us/research/people/srinath/): In Nova, soundness // error ≤ 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this // case, we keep the size of S close to 2^128. -// TODO: experimental design +/// [`AbstractNova2`] implements the Nova folding scheme which can operate on +/// both the primary and secondary curves. +/// +/// This design is experimental, following the definition of accumulation +/// schemes where the incoming witnesses and instances are simply plain vectors +/// in the circuit's assignments. pub struct AbstractNova2 { _t: PhantomData<(CM, TF)>, } +/// [`Nova2`] is the main Nova folding scheme on the primary curve. pub type Nova2 = AbstractNova2::Scalar, CHALLENGE_BITS>; +/// [`CycleFoldNova2`] is the Nova folding scheme on the secondary curve which +/// can be used as the folding scheme for folding CycleFold instances. pub type CycleFoldNova2 = AbstractNova2::Commitment>, CHALLENGE_BITS>; @@ -223,6 +242,7 @@ impl Fol type Proof = (CM::Commitment, CM::Commitment); } +/// [`AbstractNovaGadget`] is the in-circuit gadget for [`AbstractNova`]. pub struct AbstractNovaGadget { _vc: PhantomData, } diff --git a/crates/fs/src/nova/witnesses/circuits.rs b/crates/fs/src/nova/witnesses/circuits.rs index ee8554fd0..b85ea26f5 100644 --- a/crates/fs/src/nova/witnesses/circuits.rs +++ b/crates/fs/src/nova/witnesses/circuits.rs @@ -1,3 +1,5 @@ +//! In-circuit variables for Nova witnesses. + use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, @@ -8,11 +10,17 @@ use sonobe_primitives::commitments::CommitmentDefGadget; use super::{IncomingWitness, RunningWitness}; +/// [`RunningWitnessVar`] defines Nova's running witness variable. #[derive(Debug, PartialEq)] pub struct RunningWitnessVar { + /// [`RunningWitnessVar::e`] is the error term. pub e: Vec, + /// [`RunningWitnessVar::r_e`] is the randomness for the error term + /// commitment. pub r_e: CM::RandomnessVar, + /// [`RunningWitnessVar::w`] is the vector of witnesses (to the circuit). pub w: Vec, + /// [`RunningWitnessVar::r_w`] is the randomness for the witness commitment. pub r_w: CM::RandomnessVar, } @@ -57,9 +65,13 @@ impl GR1CSVar for RunningWitnessVa } } +/// [`IncomingWitnessVar`] defines Nova's incoming witness variable. #[derive(Debug, PartialEq)] pub struct IncomingWitnessVar { + /// [`IncomingWitnessVar::w`] is the vector of witnesses (to the circuit). pub w: Vec, + /// [`IncomingWitnessVar::r_w`] is the randomness for the witness + /// commitment. pub r_w: CM::RandomnessVar, } diff --git a/crates/fs/src/nova/witnesses/mod.rs b/crates/fs/src/nova/witnesses/mod.rs index 583abfebd..5f144df36 100644 --- a/crates/fs/src/nova/witnesses/mod.rs +++ b/crates/fs/src/nova/witnesses/mod.rs @@ -1,14 +1,22 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Nova +//! witnesses. + use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; use crate::FoldingWitness; pub mod circuits; +/// [`RunningWitness`] defines Nova's running witness. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningWitness { + /// [`RunningWitness::e`] is the error term. pub e: Vec, + /// [`RunningWitness::r_e`] is the randomness for the error term commitment. pub r_e: CM::Randomness, + /// [`RunningWitness::w`] is the vector of witnesses (to the circuit). pub w: Vec, + /// [`RunningWitness::r_w`] is the randomness for the witness commitment. pub r_w: CM::Randomness, } @@ -31,9 +39,12 @@ impl Dummy<&Cfg> for RunningWitness { } } +/// [`IncomingWitness`] defines Nova's incoming witness. #[derive(Clone, Debug, Eq, PartialEq)] pub struct IncomingWitness { + /// [`IncomingWitness::w`] is the witness (to the circuit). pub w: Vec, + /// [`IncomingWitness::r_w`] is the randomness for the witness commitment. pub r_w: CM::Randomness, } diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs index 88eab910a..dec49d0d6 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -1,3 +1,5 @@ +//! Nova CycleFold adapter that bridges Nova into the CycleFold IVC compiler. + use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean, @@ -22,7 +24,7 @@ use crate::compilers::cyclefold::{ CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, }; -/// Configuration for Nova's CycleFold circuit +/// [`NovaCycleFoldCircuit`] defines CycleFold circuit for Nova. pub struct NovaCycleFoldCircuit { r: Vec, points: Vec, @@ -193,6 +195,8 @@ impl FoldingSchemeCycleFo } } +/// [`NovaNovaIVC`] defines a CycleFold-based IVC using Nova as the primary +/// folding scheme and Nova as the secondary folding scheme. pub type NovaNovaIVC = CycleFoldBasedIVC, CycleFoldNova, T>; From 1a8e927c75b80ed1760f2cdcc70f911380a18e36 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 04:39:50 +0800 Subject: [PATCH 81/93] Refactor: start porting Ova --- crates/fs/src/lib.rs | 1 + crates/fs/src/ova/instance.rs | 48 +++++ crates/fs/src/ova/mod.rs | 318 ++++++++++++++++++++++++++++++++++ crates/fs/src/ova/witness.rs | 26 +++ 4 files changed, 393 insertions(+) create mode 100644 crates/fs/src/ova/instance.rs create mode 100644 crates/fs/src/ova/mod.rs create mode 100644 crates/fs/src/ova/witness.rs diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index a347a133a..1c0f0b8fb 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -26,6 +26,7 @@ pub mod definitions; pub mod nova; +pub mod ova; pub use self::definitions::{ FoldingSchemeDef, FoldingSchemeDefGadget, diff --git a/crates/fs/src/ova/instance.rs b/crates/fs/src/ova/instance.rs new file mode 100644 index 000000000..0f30a6b75 --- /dev/null +++ b/crates/fs/src/ova/instance.rs @@ -0,0 +1,48 @@ +use ark_ff::PrimeField; +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, + transcripts::Absorbable, +}; + +use crate::FoldingInstance; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningInstance { + pub u: CM::Scalar, + pub cm: CM::Commitment, + pub x: Vec, +} + +impl FoldingInstance for RunningInstance { + const N_COMMITMENTS: usize = 1; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for RunningInstance { + fn dummy(cfg: &Cfg) -> Self { + Self { + u: Default::default(), + cm: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs()], + } + } +} + +impl Absorbable for RunningInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.u.absorb_into(dest); + self.x.absorb_into(dest); + self.cm.absorb_into(dest); + } +} diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs new file mode 100644 index 000000000..7d33e471d --- /dev/null +++ b/crates/fs/src/ova/mod.rs @@ -0,0 +1,318 @@ +use ark_ff::One; +use ark_std::{ + UniformRand, borrow::Borrow, cfg_iter, marker::PhantomData, ops::Mul, rand::RngCore, sync::Arc, +}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, + arithmetizations::{ + Arith, ArithConfig, ArithRelation, + r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, + }, + circuits::{Assignments, AssignmentsOwned}, + commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, + relations::{Relation, WitnessInstanceSampler}, + transcripts::Transcript, +}; + +use self::{instance::RunningInstance as RU, witness::RunningWitness as RW}; +use crate::{ + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeKeyGenerator, FoldingSchemePreprocessor, + FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as IU, PlainWitness as IW, +}; + +pub mod instance; +pub mod witness; + +#[derive(Clone)] +pub struct OvaKey { + arith: Arc, + ck: Arc, +} + +impl DeciderKey for OvaKey { + type ProverKey = Self; + type VerifierKey = (); + type ArithConfig = A::Config; + + fn to_pk(&self) -> &Self::ProverKey { + self + } + + fn to_vk(&self) -> &Self::VerifierKey { + &() + } + + fn to_arith_config(&self) -> &Self::ArithConfig { + self.arith.config() + } +} + +impl Relation, RU> for OvaKey +where + A: for<'a> ArithRelation< + RelaxedWitness<&'a [CM::Scalar]>, + RelaxedInstance<&'a [CM::Scalar]>, + Evaluation = Vec, + >, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &RW, u: &RU) -> Result<(), Self::Error> { + let e = self.arith.eval_relation( + &RelaxedWitness { w: &w.w, e: &[] }, + &RelaxedInstance { x: &u.x, u: &u.u }, + )?; + CM::open(&self.ck, &[&w.w[..], &e].concat(), &w.r, &u.cm)?; + Ok(()) + } +} + +impl Relation, IU> for OvaKey +where + A: ArithRelation, Vec>, + CM: CommitmentDef, +{ + type Error = Error; + + fn check_relation(&self, w: &IW, u: &IU) -> Result<(), Self::Error> { + self.arith.check_relation(w, u)?; + Ok(()) + } +} + +impl WitnessInstanceSampler, IU> + for OvaKey +{ + type Source = AssignmentsOwned; + type Error = Error; + + fn sample( + &self, + z: Self::Source, + _rng: impl RngCore, + ) -> Result<(IW, IU), Error> { + Ok((z.private.into(), z.public.into())) + } +} + +impl WitnessInstanceSampler, RU> for OvaKey +where + A: for<'a> ArithRelation< + RelaxedWitness<&'a [CM::Scalar]>, + RelaxedInstance<&'a [CM::Scalar]>, + Evaluation = Vec, + >, + CM: CommitmentOps, +{ + type Source = (); + type Error = Error; + + fn sample(&self, _: Self::Source, mut rng: impl RngCore) -> Result<(RW, RU), Error> { + let cfg = self.arith.config(); + + let u = CM::Scalar::rand(&mut rng); + let x = (0..cfg.n_public_inputs()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let w = (0..cfg.n_witnesses()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let e = self.arith.eval_relation( + &RelaxedWitness { w: &w, e: &[] }, + &RelaxedInstance { x: &x, u: &u }, + )?; + + let (cm, r) = CM::commit(&self.ck, &[&w[..], &e].concat(), &mut rng)?; + Ok((RW { w, r }, RU { x, cm, u })) + } +} + +pub struct Ova { + _vc: PhantomData, +} + +impl FoldingSchemeDef + for Ova +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = IW; + type IU = IU; + + type TranscriptField = CM::Scalar; + type Arith = R1CS; + + type Config = (usize, usize); + type PublicParam = CM::Key; + type DeciderKey = OvaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = CM::Commitment; +} + +impl FoldingSchemePreprocessor + for Ova +{ + fn preprocess( + (n_constraints, n_witnesses): (usize, usize), + mut rng: impl RngCore, + ) -> Result { + let ck = CM::generate_key(n_constraints + n_witnesses, &mut rng)?; + Ok(ck) + } +} + +impl FoldingSchemeKeyGenerator + for Ova +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + let cfg = r1cs.config(); + if ck.max_scalars_len() < cfg.n_constraints() + cfg.n_witnesses() { + return Err(Error::InvalidPublicParameters( + "The commitment key generated by preprocessing is too short for the R1CS instance" + .into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} + +impl FoldingSchemeProver<1, 1> + for Ova +{ + fn prove( + pk: &OvaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Compute the cross term `T` by following the original Nova paper. + let z1 = Assignments::from((U.u, &U.x, &W.w)); + let z2 = Assignments::from((CM::Scalar::one(), &u[..], &w[..])); + let t = pk.arith.evaluate_rows(|((a, b), c)| { + let az1: CM::Scalar = a.iter().map(|(val, col)| z1[*col] * val).sum(); + let az2: CM::Scalar = a.iter().map(|(val, col)| z2[*col] * val).sum(); + let bz1: CM::Scalar = b.iter().map(|(val, col)| z1[*col] * val).sum(); + let bz2: CM::Scalar = b.iter().map(|(val, col)| z2[*col] * val).sum(); + let cz1: CM::Scalar = c.iter().map(|(val, col)| z1[*col] * val).sum(); + let cz2: CM::Scalar = c.iter().map(|(val, col)| z2[*col] * val).sum(); + Ok(az1 * bz2 + az2 * bz1 - z2[0] * cz1 - z1[0] * cz2) + })?; + + let (cm, r) = CM::commit(&pk.ck, &[w, &t[..]].concat(), rng)?; + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&cm); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(( + RW { + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r: W.r + r * rho, + }, + RU { + u: U.u + rho, + cm: U.cm + cm.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + cm, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeVerifier<1, 1> + for Ova +{ + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + cm: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(cm); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(RU { + u: U.u + rho, + cm: U.cm + cm.mul(rho), + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective}; + use ark_ff::UniformRand; + use ark_std::{error::Error, test_rng}; + use sonobe_primitives::{ + circuits::utils::{CircuitForTest, satisfying_assignments_for_test}, + commitments::pedersen::Pedersen, + }; + + use super::*; + use crate::tests::test_folding_scheme; + + #[test] + fn test_ova() -> Result<(), Box> { + let mut rng = test_rng(); + + let config = (4, 4); + + test_folding_scheme::>, 1, 1>( + config, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..10) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::>, 1, 1>( + config, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..10) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + Ok(()) + } +} diff --git a/crates/fs/src/ova/witness.rs b/crates/fs/src/ova/witness.rs new file mode 100644 index 000000000..624bfad41 --- /dev/null +++ b/crates/fs/src/ova/witness.rs @@ -0,0 +1,26 @@ +use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; + +use crate::FoldingWitness; + +#[derive(Debug, PartialEq)] +pub struct RunningWitness { + pub w: Vec, + pub r: CM::Randomness, +} + +impl FoldingWitness for RunningWitness { + const N_OPENINGS: usize = 1; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.w, &self.r)] + } +} + +impl Dummy<&Cfg> for RunningWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses()], + r: Default::default(), + } + } +} From 32d2cf77cef7582a09f7b1c19c2f8027c0dd4e1a Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 18:43:27 +0800 Subject: [PATCH 82/93] Refactor: allow Ova to have any transcript field --- crates/fs/src/ova/mod.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs index 7d33e471d..749a4f452 100644 --- a/crates/fs/src/ova/mod.rs +++ b/crates/fs/src/ova/mod.rs @@ -13,6 +13,7 @@ use sonobe_primitives::{ circuits::{Assignments, AssignmentsOwned}, commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, + traits::SonobeField, transcripts::Transcript, }; @@ -130,12 +131,15 @@ where } } -pub struct Ova { - _vc: PhantomData, +pub struct AbstractOva { + _t: PhantomData<(CM, TF)>, } -impl FoldingSchemeDef - for Ova +pub type Ova = + AbstractOva::Scalar, CHALLENGE_BITS>; + +impl FoldingSchemeDef + for AbstractOva { type CM = CM; type RW = RW; @@ -143,7 +147,7 @@ impl FoldingSchemeDef type IW = IW; type IU = IU; - type TranscriptField = CM::Scalar; + type TranscriptField = TF; type Arith = R1CS; type Config = (usize, usize); @@ -153,8 +157,8 @@ impl FoldingSchemeDef type Proof = CM::Commitment; } -impl FoldingSchemePreprocessor - for Ova +impl + FoldingSchemePreprocessor for AbstractOva { fn preprocess( (n_constraints, n_witnesses): (usize, usize), @@ -165,8 +169,8 @@ impl FoldingSchemePreproc } } -impl FoldingSchemeKeyGenerator - for Ova +impl + FoldingSchemeKeyGenerator for AbstractOva { fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { let ck = Arc::new(ck); @@ -182,12 +186,12 @@ impl FoldingSchemeKeyGene } } -impl FoldingSchemeProver<1, 1> - for Ova +impl + FoldingSchemeProver<1, 1> for AbstractOva { fn prove( pk: &OvaKey, - transcript: &mut impl Transcript, + transcript: &mut impl Transcript, Ws: &[impl Borrow; 1], Us: &[impl Borrow; 1], ws: &[impl Borrow; 1], @@ -242,12 +246,12 @@ impl FoldingSchemeProver< } } -impl FoldingSchemeVerifier<1, 1> - for Ova +impl + FoldingSchemeVerifier<1, 1> for AbstractOva { fn verify( _vk: &(), - transcript: &mut impl Transcript, + transcript: &mut impl Transcript, Us: &[impl Borrow; 1], us: &[impl Borrow; 1], cm: &Self::Proof<1, 1>, From 4ba99494ff7234be60f35a099ed15b8e68ac511c Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 10 Oct 2025 19:04:22 +0800 Subject: [PATCH 83/93] Prepare for cyclefold --- crates/fs/src/ova/mod.rs | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs index 749a4f452..09ae05694 100644 --- a/crates/fs/src/ova/mod.rs +++ b/crates/fs/src/ova/mod.rs @@ -13,7 +13,7 @@ use sonobe_primitives::{ circuits::{Assignments, AssignmentsOwned}, commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, - traits::SonobeField, + traits::{CF2, SonobeField}, transcripts::Transcript, }; @@ -135,8 +135,11 @@ pub struct AbstractOva { _t: PhantomData<(CM, TF)>, } -pub type Ova = - AbstractOva::Scalar, CHALLENGE_BITS>; +pub type Ova = + AbstractOva::Scalar, CHALLENGE_BITS>; + +pub type CycleFoldOva = + AbstractOva::Commitment>, CHALLENGE_BITS>; impl FoldingSchemeDef for AbstractOva @@ -279,7 +282,7 @@ impl #[cfg(test)] mod tests { - use ark_bn254::{Fr, G1Projective}; + use ark_bn254::{Fq, Fr, G1Projective}; use ark_ff::UniformRand; use ark_std::{error::Error, test_rng}; use sonobe_primitives::{ @@ -290,33 +293,42 @@ mod tests { use super::*; use crate::tests::test_folding_scheme; - #[test] - fn test_ova() -> Result<(), Box> { - let mut rng = test_rng(); - + fn test_ova_opt( + rounds: usize, + mut rng: impl RngCore, + ) -> Result<(), Box> { let config = (4, 4); - test_folding_scheme::>, 1, 1>( + test_folding_scheme::, TF>, 1, 1>( config, CircuitForTest { x: Fr::rand(&mut rng), }, - (0..10) + (0..rounds) .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) .collect(), &mut rng, )?; - test_folding_scheme::>, 1, 1>( + test_folding_scheme::, TF>, 1, 1>( config, CircuitForTest { x: Fr::rand(&mut rng), }, - (0..10) + (0..rounds) .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) .collect(), &mut rng, )?; Ok(()) } + + #[test] + fn test_ova() -> Result<(), Box> { + let mut rng = test_rng(); + + test_ova_opt::(10, &mut rng)?; + test_ova_opt::(10, &mut rng)?; + Ok(()) + } } From 69c00183255ab610e05f0c55a679a45875ed6910 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 24 Oct 2025 20:49:56 +0800 Subject: [PATCH 84/93] In circuit variables for Ova instances and witnesses --- crates/fs/src/ova/instance/circuits.rs | 111 +++++++++++++++++ .../src/ova/{instance.rs => instance/mod.rs} | 2 + crates/fs/src/ova/mod.rs | 117 +++++++++++++++++- crates/fs/src/ova/witness/circuits.rs | 48 +++++++ .../fs/src/ova/{witness.rs => witness/mod.rs} | 4 +- 5 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 crates/fs/src/ova/instance/circuits.rs rename crates/fs/src/ova/{instance.rs => instance/mod.rs} (98%) create mode 100644 crates/fs/src/ova/witness/circuits.rs rename crates/fs/src/ova/{witness.rs => witness/mod.rs} (92%) diff --git a/crates/fs/src/ova/instance/circuits.rs b/crates/fs/src/ova/instance/circuits.rs new file mode 100644 index 000000000..e0b58af52 --- /dev/null +++ b/crates/fs/src/ova/instance/circuits.rs @@ -0,0 +1,111 @@ +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + prelude::Boolean, + select::CondSelectGadget, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::AbsorbableVar}; + +use super::RunningInstance; +use crate::FoldingInstanceVar; + +#[derive(Clone, Debug, PartialEq)] +pub struct RunningInstanceVar { + pub u: CM::ScalarVar, + pub cm: CM::CommitmentVar, + pub x: Vec, +} + +impl AllocVar, CM::ConstraintField> + for RunningInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningInstance { u, cm, x } = v.borrow(); + Ok(Self { + u: AllocVar::new_variable(cs.clone(), || Ok(u), mode)?, + cm: AllocVar::new_variable(cs.clone(), || Ok(cm), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for RunningInstanceVar { + type Value = RunningInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.u.cs().or(self.cm.cs()).or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(RunningInstance { + u: self.u.value()?, + cm: self.cm.value()?, + x: self.x.value()?, + }) + } +} + +impl AbsorbableVar for RunningInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.u.absorb_into(dest)?; + self.x.absorb_into(dest)?; + self.cm.absorb_into(dest) + } +} + +impl CondSelectGadget for RunningInstanceVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.x.len() != false_value.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self { + u: cond.select(&true_value.u, &false_value.u)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + cm: cond.select(&true_value.cm, &false_value.cm)?, + }) + } +} + +impl FoldingInstanceVar for RunningInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm] + } + + fn public_inputs(&self) -> &Vec { + &self.x + } + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result { + let cs = cs.into().cs(); + Ok(Self { + u: AllocVar::new_witness(cs.clone(), || Ok(&u.u))?, + cm: AllocVar::new_witness(cs.clone(), || Ok(&u.cm))?, + x, + }) + } +} diff --git a/crates/fs/src/ova/instance.rs b/crates/fs/src/ova/instance/mod.rs similarity index 98% rename from crates/fs/src/ova/instance.rs rename to crates/fs/src/ova/instance/mod.rs index 0f30a6b75..283057bf8 100644 --- a/crates/fs/src/ova/instance.rs +++ b/crates/fs/src/ova/instance/mod.rs @@ -6,6 +6,8 @@ use sonobe_primitives::{ use crate::FoldingInstance; +pub mod circuits; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningInstance { pub u: CM::Scalar, diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs index 09ae05694..e32b2b09f 100644 --- a/crates/fs/src/ova/mod.rs +++ b/crates/fs/src/ova/mod.rs @@ -1,26 +1,35 @@ use ark_ff::One; +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, groups::CurveVar}; +use ark_relations::gr1cs::SynthesisError; use ark_std::{ UniformRand, borrow::Borrow, cfg_iter, marker::PhantomData, ops::Mul, rand::RngCore, sync::Arc, }; #[cfg(feature = "parallel")] use rayon::prelude::*; use sonobe_primitives::{ - algebra::ops::bits::FromBits, + algebra::ops::bits::{FromBits, FromBitsGadget}, arithmetizations::{ Arith, ArithConfig, ArithRelation, r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, }, circuits::{Assignments, AssignmentsOwned}, - commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, + commitments::{ + CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, GroupBasedCommitment, + }, relations::{Relation, WitnessInstanceSampler}, traits::{CF2, SonobeField}, - transcripts::Transcript, + transcripts::{Transcript, TranscriptGadget}, }; -use self::{instance::RunningInstance as RU, witness::RunningWitness as RW}; +use self::{ + instance::{RunningInstance as RU, circuits::RunningInstanceVar as RUVar}, + witness::RunningWitness as RW, +}; use crate::{ - DeciderKey, Error, FoldingSchemeDef, FoldingSchemeKeyGenerator, FoldingSchemePreprocessor, - FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as IU, PlainWitness as IW, + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, + FoldingSchemeKeyGenerator, FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, + FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as IU, PlainInstanceVar as IUVar, + PlainWitness as IW, }; pub mod instance; @@ -280,6 +289,102 @@ impl } } +pub struct AbstractOvaGadget { + _t: PhantomData, +} + +impl FoldingSchemeDefGadget + for AbstractOvaGadget +where + CM: CommitmentDefGadget, +{ + type Widget = AbstractOva; + + type CM = CM; + type RU = RUVar; + type IU = IUVar; + type VerifierKey = (); + type Challenge = [Boolean; CHALLENGE_BITS]; + type Proof = CM::CommitmentVar; +} + +impl FoldingSchemePartialVerifierGadget<1, 1> + for AbstractOvaGadget +where + CM: CommitmentDefGadget, +{ + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm: CM::CommitmentVar::new_witness(U.cm.cs().or(proof.cs()).or(rho.cs()), || { + Ok(U.cm.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) + })?, + x: U.x + .iter() + .zip(&u[..]) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeFullVerifierGadget<1, 1> + for AbstractOvaGadget +where + CM: CommitmentDefGadget, + CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, +{ + fn verify( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm: proof.scalar_mul_le(rho_bits.iter())? + &U.cm, + x: U.x + .iter() + .zip(&u[..]) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} + #[cfg(test)] mod tests { use ark_bn254::{Fq, Fr, G1Projective}; diff --git a/crates/fs/src/ova/witness/circuits.rs b/crates/fs/src/ova/witness/circuits.rs new file mode 100644 index 000000000..0a7652fc6 --- /dev/null +++ b/crates/fs/src/ova/witness/circuits.rs @@ -0,0 +1,48 @@ +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::commitments::CommitmentDefGadget; + +use super::RunningWitness; + +#[derive(Debug, PartialEq)] +pub struct RunningWitnessVar { + pub w: Vec, + pub r: CM::RandomnessVar, +} + +impl AllocVar, CM::ConstraintField> + for RunningWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningWitness { w, r } = v.borrow(); + Ok(Self { + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r: AllocVar::new_variable(cs.clone(), || Ok(r), mode)?, + }) + } +} + +impl GR1CSVar for RunningWitnessVar { + type Value = RunningWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.w.cs().or(self.r.cs()) + } + + fn value(&self) -> Result { + Ok(RunningWitness { + w: self.w.value()?, + r: self.r.value()?, + }) + } +} diff --git a/crates/fs/src/ova/witness.rs b/crates/fs/src/ova/witness/mod.rs similarity index 92% rename from crates/fs/src/ova/witness.rs rename to crates/fs/src/ova/witness/mod.rs index 624bfad41..a20c1a854 100644 --- a/crates/fs/src/ova/witness.rs +++ b/crates/fs/src/ova/witness/mod.rs @@ -2,7 +2,9 @@ use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDe use crate::FoldingWitness; -#[derive(Debug, PartialEq)] +pub mod circuits; + +#[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningWitness { pub w: Vec, pub r: CM::Randomness, From fbb2b62229839a5984a88b2e4689f80e73c9e806 Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 8 Nov 2025 08:47:34 +0800 Subject: [PATCH 85/93] Ova adapter --- crates/fs/src/ova/mod.rs | 25 ++- .../src/compilers/cyclefold/adapters/mod.rs | 1 + .../src/compilers/cyclefold/adapters/nova.rs | 22 +++ .../src/compilers/cyclefold/adapters/ova.rs | 154 ++++++++++++++++++ 4 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 crates/ivc/src/compilers/cyclefold/adapters/ova.rs diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs index e32b2b09f..54a094717 100644 --- a/crates/fs/src/ova/mod.rs +++ b/crates/fs/src/ova/mod.rs @@ -26,10 +26,7 @@ use self::{ witness::RunningWitness as RW, }; use crate::{ - DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, - FoldingSchemeKeyGenerator, FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, - FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as IU, PlainInstanceVar as IUVar, - PlainWitness as IW, + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, FoldingSchemeKeyGenerator, FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, GroupBasedFoldingSchemePrimaryDef, GroupBasedFoldingSchemeSecondaryDef, PlainInstance as IU, PlainInstanceVar as IUVar, PlainWitness as IW }; pub mod instance; @@ -201,6 +198,7 @@ impl impl FoldingSchemeProver<1, 1> for AbstractOva { + #[allow(non_snake_case)] fn prove( pk: &OvaKey, transcript: &mut impl Transcript, @@ -246,7 +244,7 @@ impl }, RU { u: U.u + rho, - cm: U.cm + cm.mul(rho), + cm: U.cm + cm * rho, x: cfg_iter!(U.x) .zip(&u[..]) .map(|(a, b)| rho * b + a) @@ -261,6 +259,7 @@ impl impl FoldingSchemeVerifier<1, 1> for AbstractOva { + #[allow(non_snake_case)] fn verify( _vk: &(), transcript: &mut impl Transcript, @@ -280,7 +279,7 @@ impl Ok(RU { u: U.u + rho, - cm: U.cm + cm.mul(rho), + cm: U.cm + *cm * rho, x: cfg_iter!(U.x) .zip(&u[..]) .map(|(a, b)| rho * b + a) @@ -313,6 +312,7 @@ impl FoldingSchemePartialVerifierGadget<1, 1> where CM: CommitmentDefGadget, { + #[allow(non_snake_case)] fn verify_hinted( _vk: &Self::VerifierKey, transcript: &mut impl TranscriptGadget, @@ -355,6 +355,7 @@ where CM: CommitmentDefGadget, CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, { + #[allow(non_snake_case)] fn verify( _vk: &Self::VerifierKey, transcript: &mut impl TranscriptGadget, @@ -385,6 +386,18 @@ where } } +impl GroupBasedFoldingSchemePrimaryDef + for AbstractOva +{ + type Gadget = AbstractOvaGadget; +} + +impl GroupBasedFoldingSchemeSecondaryDef + for AbstractOva, CHALLENGE_BITS> +{ + type Gadget = AbstractOvaGadget; +} + #[cfg(test)] mod tests { use ark_bn254::{Fq, Fr, G1Projective}; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/mod.rs b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs index cfbca4153..8994ba2cd 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs @@ -2,3 +2,4 @@ //! folding schemes. pub mod nova; +pub mod ova; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs index dec49d0d6..a2d199a42 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/nova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/nova.rs @@ -9,6 +9,7 @@ use ark_std::{borrow::Borrow, iter::once}; use sonobe_fs::{ FoldingSchemeDefGadget, nova::{CycleFoldNova, Nova}, + ova::CycleFoldOva, }; use sonobe_primitives::{ algebra::{ @@ -195,6 +196,11 @@ impl FoldingSchemeCycleFo } } +/// [`NovaOvaIVC`] defines a CycleFold-based IVC using Nova as the primary +/// folding scheme and Ova as the secondary folding scheme. +pub type NovaOvaIVC = + CycleFoldBasedIVC, CycleFoldOva, T>; + /// [`NovaNovaIVC`] defines a CycleFold-based IVC using Nova as the primary /// folding scheme and Nova as the secondary folding scheme. pub type NovaNovaIVC = @@ -217,6 +223,22 @@ mod tests { use super::*; use crate::tests::test_ivc; + #[test] + fn test_nova_ova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + (65536, (2048, 2048), Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } + #[test] fn test_nova_nova() -> Result<(), Box> { let mut rng = thread_rng(); diff --git a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs new file mode 100644 index 000000000..25f6dacd7 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs @@ -0,0 +1,154 @@ +use ark_ff::{PrimeField, Zero}; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean}; +use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::{borrow::Borrow, iter::once}; +use sonobe_fs::{ + FoldingSchemeDefGadget, + nova::CycleFoldNova, + ova::{CycleFoldOva, Ova}, +}; +use sonobe_primitives::{ + algebra::{ + field::emulated::{Bounds, EmulatedFieldVar}, + ops::bits::{FromBits, FromBitsGadget, ToBitsGadgetExt}, + }, + commitments::GroupBasedCommitment, + traits::{CF2, SonobeCurve}, +}; + +use crate::compilers::cyclefold::{ + CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, +}; + +/// Configuration for Ova's CycleFold circuit +pub struct OvaCycleFoldCircuit { + r: Vec, + points: Vec, +} + +impl Default + for OvaCycleFoldCircuit +{ + fn default() -> Self { + Self { + r: vec![false; CHALLENGE_BITS], + points: vec![C::zero(); 2], + } + } +} + +impl CycleFoldCircuit> + for OvaCycleFoldCircuit +{ + fn verify_point_rlc(&self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let rho = FpVar::new_input(cs.clone(), || Ok(CF2::::from_bits_le(&self.r[..])))?; + let rho_bits = rho.to_n_bits_le(CHALLENGE_BITS)?; + + let points = Vec::::new_witness(cs.clone(), || Ok(&self.points[..]))?; + for point in &points { + Self::mark_point_as_public(point)?; + } + + Self::mark_point_as_public(&(points[1].scalar_mul_le(rho_bits.iter())? + &points[0])) + } +} + +impl FoldingSchemeCycleFoldExt<1, 1> + for Ova +{ + const N_CYCLEFOLDS: usize = 1; + + type CFCircuit = OvaCycleFoldCircuit; + + fn to_cyclefold_circuits( + [U]: &[impl Borrow; 1], + _us: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + rho: Self::Challenge, + ) -> Vec { + vec![OvaCycleFoldCircuit { + r: rho.into(), + points: vec![U.borrow().cm, *proof], + }] + } + + fn to_cyclefold_inputs( + [U]: [::RU; 1], + _us: [::IU; 1], + UU: ::RU, + proof: ::Proof<1, 1>, + rho: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho = rho.to_vec(); + rho.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + Ok(vec![ + once(EmulatedFieldVar::from_bounded_bits_le( + &rho, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?) + .chain([U.cm, proof, UU.cm].into_iter().flat_map(|p| [p.x, p.y])) + .collect(), + ]) + } +} + +pub type OvaOvaIVC = + CycleFoldBasedIVC, CycleFoldOva, T>; + +pub type OvaNovaIVC = + CycleFoldBasedIVC, CycleFoldNova, T>; + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective as C1}; + use ark_ff::UniformRand; + use ark_grumpkin::Projective as C2; + use ark_std::{error::Error, rand::thread_rng, sync::Arc}; + use sonobe_primitives::{ + circuits::utils::CircuitForTest, + commitments::pedersen::Pedersen, + transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, + }; + + use super::*; + use crate::tests::test_ivc; + + #[test] + fn test_ova_ova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + ( + (65536, 65536), + (2048, 2048), + Arc::new(GriffinParams::new(16, 5, 9)), + ), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } + + #[test] + fn test_ova_nova() -> Result<(), Box> { + let mut rng = thread_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + ((65536, 65536), 2048, Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } +} From 3b615875d5624d14c0544824ae6501722100ade5 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 04:50:00 +0800 Subject: [PATCH 86/93] Split impls into separate submodules --- crates/fs/src/ova/algorithms/key_generator.rs | 25 ++ crates/fs/src/ova/algorithms/mod.rs | 4 + crates/fs/src/ova/algorithms/preprocessor.rs | 16 ++ crates/fs/src/ova/algorithms/prover.rs | 74 ++++++ crates/fs/src/ova/algorithms/verifier.rs | 41 ++++ crates/fs/src/ova/circuits/mod.rs | 1 + crates/fs/src/ova/circuits/verifier.rs | 90 +++++++ .../ova/{instance => instances}/circuits.rs | 2 +- .../fs/src/ova/{instance => instances}/mod.rs | 0 crates/fs/src/ova/mod.rs | 231 +----------------- .../ova/{witness => witnesses}/circuits.rs | 0 .../fs/src/ova/{witness => witnesses}/mod.rs | 0 .../src/compilers/cyclefold/adapters/ova.rs | 2 + 13 files changed, 267 insertions(+), 219 deletions(-) create mode 100644 crates/fs/src/ova/algorithms/key_generator.rs create mode 100644 crates/fs/src/ova/algorithms/mod.rs create mode 100644 crates/fs/src/ova/algorithms/preprocessor.rs create mode 100644 crates/fs/src/ova/algorithms/prover.rs create mode 100644 crates/fs/src/ova/algorithms/verifier.rs create mode 100644 crates/fs/src/ova/circuits/mod.rs create mode 100644 crates/fs/src/ova/circuits/verifier.rs rename crates/fs/src/ova/{instance => instances}/circuits.rs (99%) rename crates/fs/src/ova/{instance => instances}/mod.rs (100%) rename crates/fs/src/ova/{witness => witnesses}/circuits.rs (100%) rename crates/fs/src/ova/{witness => witnesses}/mod.rs (100%) diff --git a/crates/fs/src/ova/algorithms/key_generator.rs b/crates/fs/src/ova/algorithms/key_generator.rs new file mode 100644 index 000000000..7cfff3f3d --- /dev/null +++ b/crates/fs/src/ova/algorithms/key_generator.rs @@ -0,0 +1,25 @@ +use ark_std::sync::Arc; +use sonobe_primitives::{ + arithmetizations::{Arith, ArithConfig}, + commitments::{CommitmentKey, GroupBasedCommitment}, + traits::SonobeField, +}; + +use crate::{Error, FoldingSchemeKeyGenerator, ova::AbstractOva}; + +impl + FoldingSchemeKeyGenerator for AbstractOva +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + let cfg = r1cs.config(); + if ck.max_scalars_len() < cfg.n_constraints() + cfg.n_witnesses() { + return Err(Error::InvalidPublicParameters( + "The commitment key generated by preprocessing is too short for the R1CS instance" + .into(), + )); + } + Ok(Self::DeciderKey { arith: r1cs, ck }) + } +} diff --git a/crates/fs/src/ova/algorithms/mod.rs b/crates/fs/src/ova/algorithms/mod.rs new file mode 100644 index 000000000..11e838298 --- /dev/null +++ b/crates/fs/src/ova/algorithms/mod.rs @@ -0,0 +1,4 @@ +pub mod key_generator; +pub mod preprocessor; +pub mod prover; +pub mod verifier; diff --git a/crates/fs/src/ova/algorithms/preprocessor.rs b/crates/fs/src/ova/algorithms/preprocessor.rs new file mode 100644 index 000000000..d5e988c91 --- /dev/null +++ b/crates/fs/src/ova/algorithms/preprocessor.rs @@ -0,0 +1,16 @@ +use ark_std::rand::RngCore; +use sonobe_primitives::{commitments::GroupBasedCommitment, traits::SonobeField}; + +use crate::{Error, FoldingSchemePreprocessor, ova::AbstractOva}; + +impl + FoldingSchemePreprocessor for AbstractOva +{ + fn preprocess( + (n_constraints, n_witnesses): (usize, usize), + mut rng: impl RngCore, + ) -> Result { + let ck = CM::generate_key(n_constraints + n_witnesses, &mut rng)?; + Ok(ck) + } +} diff --git a/crates/fs/src/ova/algorithms/prover.rs b/crates/fs/src/ova/algorithms/prover.rs new file mode 100644 index 000000000..1941951bd --- /dev/null +++ b/crates/fs/src/ova/algorithms/prover.rs @@ -0,0 +1,74 @@ +use ark_ff::One; +use ark_std::{borrow::Borrow, cfg_iter, rand::RngCore}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, circuits::Assignments, commitments::GroupBasedCommitment, + traits::SonobeField, transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeProver, + ova::{AbstractOva, OvaKey}, +}; + +impl + FoldingSchemeProver<1, 1> for AbstractOva +{ + #[allow(non_snake_case)] + fn prove( + pk: &OvaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Compute the cross term `T` by following the original Nova paper. + let z1 = Assignments::from((U.u, &U.x, &W.w)); + let z2 = Assignments::from((CM::Scalar::one(), &u[..], &w[..])); + let t = pk.arith.evaluate_rows(|((a, b), c)| { + let az1: CM::Scalar = a.iter().map(|(val, col)| z1[*col] * val).sum(); + let az2: CM::Scalar = a.iter().map(|(val, col)| z2[*col] * val).sum(); + let bz1: CM::Scalar = b.iter().map(|(val, col)| z1[*col] * val).sum(); + let bz2: CM::Scalar = b.iter().map(|(val, col)| z2[*col] * val).sum(); + let cz1: CM::Scalar = c.iter().map(|(val, col)| z1[*col] * val).sum(); + let cz2: CM::Scalar = c.iter().map(|(val, col)| z2[*col] * val).sum(); + Ok(az1 * bz2 + az2 * bz1 - z2[0] * cz1 - z1[0] * cz2) + })?; + + let (cm, r) = CM::commit(&pk.ck, &[w, &t[..]].concat(), rng)?; + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(&cm); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(( + Self::RW { + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r: W.r + r * rho, + }, + Self::RU { + u: U.u + rho, + cm: U.cm + cm * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + cm, + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/ova/algorithms/verifier.rs b/crates/fs/src/ova/algorithms/verifier.rs new file mode 100644 index 000000000..57f771fe3 --- /dev/null +++ b/crates/fs/src/ova/algorithms/verifier.rs @@ -0,0 +1,41 @@ +use ark_std::{borrow::Borrow, cfg_iter}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, commitments::GroupBasedCommitment, traits::SonobeField, + transcripts::Transcript, +}; + +use crate::{Error, FoldingSchemeVerifier, ova::AbstractOva}; + +impl + FoldingSchemeVerifier<1, 1> for AbstractOva +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + cm: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let rho_bits = { + transcript.add(&U); + transcript.add(&u); + transcript.add(cm); + transcript.challenge_bits(CHALLENGE_BITS) + }; + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(Self::RU { + u: U.u + rho, + cm: U.cm + *cm * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} diff --git a/crates/fs/src/ova/circuits/mod.rs b/crates/fs/src/ova/circuits/mod.rs new file mode 100644 index 000000000..9a0722027 --- /dev/null +++ b/crates/fs/src/ova/circuits/mod.rs @@ -0,0 +1 @@ +pub mod verifier; diff --git a/crates/fs/src/ova/circuits/verifier.rs b/crates/fs/src/ova/circuits/verifier.rs new file mode 100644 index 000000000..eb397a597 --- /dev/null +++ b/crates/fs/src/ova/circuits/verifier.rs @@ -0,0 +1,90 @@ +use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, groups::CurveVar}; +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + algebra::ops::bits::FromBitsGadget, + commitments::{CommitmentDef, CommitmentDefGadget, GroupBasedCommitment}, + transcripts::TranscriptGadget, +}; + +use crate::{ + FoldingSchemeFullVerifierGadget, FoldingSchemePartialVerifierGadget, ova::AbstractOvaGadget, +}; + +impl FoldingSchemePartialVerifierGadget<1, 1> + for AbstractOvaGadget +where + CM: CommitmentDefGadget, +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm: CM::CommitmentVar::new_witness(U.cm.cs().or(proof.cs()).or(rho.cs()), || { + Ok(U.cm.value().unwrap_or_default() + + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) + })?, + x: U.x + .iter() + .zip(&u[..]) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeFullVerifierGadget<1, 1> + for AbstractOvaGadget +where + CM: CommitmentDefGadget, + CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, +{ + #[allow(non_snake_case)] + fn verify( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let rho_bits = { + transcript.add(&U)?; + transcript.add(&u)?; + transcript.add(proof)?; + transcript.challenge_bits(CHALLENGE_BITS)? + }; + let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; + + Ok(Self::RU { + u: (U.u.clone() + &rho) + .try_into() + .map_err(|_| SynthesisError::Unsatisfiable)?, + cm: proof.scalar_mul_le(rho_bits.iter())? + &U.cm, + x: U.x + .iter() + .zip(&u[..]) + .map(|(a, b)| (b.clone() * &rho + a).try_into()) + .collect::>() + .map_err(|_| SynthesisError::Unsatisfiable)?, + }) + } +} diff --git a/crates/fs/src/ova/instance/circuits.rs b/crates/fs/src/ova/instances/circuits.rs similarity index 99% rename from crates/fs/src/ova/instance/circuits.rs rename to crates/fs/src/ova/instances/circuits.rs index e0b58af52..06af66ff5 100644 --- a/crates/fs/src/ova/instance/circuits.rs +++ b/crates/fs/src/ova/instances/circuits.rs @@ -1,8 +1,8 @@ use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, + boolean::Boolean, fields::fp::FpVar, - prelude::Boolean, select::CondSelectGadget, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; diff --git a/crates/fs/src/ova/instance/mod.rs b/crates/fs/src/ova/instances/mod.rs similarity index 100% rename from crates/fs/src/ova/instance/mod.rs rename to crates/fs/src/ova/instances/mod.rs diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs index 54a094717..1b90d37ea 100644 --- a/crates/fs/src/ova/mod.rs +++ b/crates/fs/src/ova/mod.rs @@ -1,36 +1,32 @@ -use ark_ff::One; -use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, boolean::Boolean, groups::CurveVar}; -use ark_relations::gr1cs::SynthesisError; -use ark_std::{ - UniformRand, borrow::Borrow, cfg_iter, marker::PhantomData, ops::Mul, rand::RngCore, sync::Arc, -}; +use ark_r1cs_std::boolean::Boolean; +use ark_std::{UniformRand, marker::PhantomData, rand::RngCore, sync::Arc}; #[cfg(feature = "parallel")] use rayon::prelude::*; use sonobe_primitives::{ - algebra::ops::bits::{FromBits, FromBitsGadget}, arithmetizations::{ Arith, ArithConfig, ArithRelation, r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, }, - circuits::{Assignments, AssignmentsOwned}, - commitments::{ - CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, GroupBasedCommitment, - }, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentDefGadget, CommitmentOps, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, traits::{CF2, SonobeField}, - transcripts::{Transcript, TranscriptGadget}, }; use self::{ - instance::{RunningInstance as RU, circuits::RunningInstanceVar as RUVar}, - witness::RunningWitness as RW, + instances::{RunningInstance as RU, circuits::RunningInstanceVar as RUVar}, + witnesses::RunningWitness as RW, }; use crate::{ - DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeFullVerifierGadget, FoldingSchemeKeyGenerator, FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, FoldingSchemeProver, FoldingSchemeVerifier, GroupBasedFoldingSchemePrimaryDef, GroupBasedFoldingSchemeSecondaryDef, PlainInstance as IU, PlainInstanceVar as IUVar, PlainWitness as IW + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, GroupBasedFoldingSchemePrimaryDef, + GroupBasedFoldingSchemeSecondaryDef, PlainInstance as IU, PlainInstanceVar as IUVar, + PlainWitness as IW, }; -pub mod instance; -pub mod witness; +pub mod algorithms; +pub mod circuits; +pub mod instances; +pub mod witnesses; #[derive(Clone)] pub struct OvaKey { @@ -166,128 +162,6 @@ impl Fol type Proof = CM::Commitment; } -impl - FoldingSchemePreprocessor for AbstractOva -{ - fn preprocess( - (n_constraints, n_witnesses): (usize, usize), - mut rng: impl RngCore, - ) -> Result { - let ck = CM::generate_key(n_constraints + n_witnesses, &mut rng)?; - Ok(ck) - } -} - -impl - FoldingSchemeKeyGenerator for AbstractOva -{ - fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { - let ck = Arc::new(ck); - let r1cs = Arc::new(r1cs); - let cfg = r1cs.config(); - if ck.max_scalars_len() < cfg.n_constraints() + cfg.n_witnesses() { - return Err(Error::InvalidPublicParameters( - "The commitment key generated by preprocessing is too short for the R1CS instance" - .into(), - )); - } - Ok(Self::DeciderKey { arith: r1cs, ck }) - } -} - -impl - FoldingSchemeProver<1, 1> for AbstractOva -{ - #[allow(non_snake_case)] - fn prove( - pk: &OvaKey, - transcript: &mut impl Transcript, - Ws: &[impl Borrow; 1], - Us: &[impl Borrow; 1], - ws: &[impl Borrow; 1], - us: &[impl Borrow; 1], - rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { - let (W, U) = (Ws[0].borrow(), Us[0].borrow()); - let (w, u) = (ws[0].borrow(), us[0].borrow()); - - // Compute the cross term `T` by following the original Nova paper. - let z1 = Assignments::from((U.u, &U.x, &W.w)); - let z2 = Assignments::from((CM::Scalar::one(), &u[..], &w[..])); - let t = pk.arith.evaluate_rows(|((a, b), c)| { - let az1: CM::Scalar = a.iter().map(|(val, col)| z1[*col] * val).sum(); - let az2: CM::Scalar = a.iter().map(|(val, col)| z2[*col] * val).sum(); - let bz1: CM::Scalar = b.iter().map(|(val, col)| z1[*col] * val).sum(); - let bz2: CM::Scalar = b.iter().map(|(val, col)| z2[*col] * val).sum(); - let cz1: CM::Scalar = c.iter().map(|(val, col)| z1[*col] * val).sum(); - let cz2: CM::Scalar = c.iter().map(|(val, col)| z2[*col] * val).sum(); - Ok(az1 * bz2 + az2 * bz1 - z2[0] * cz1 - z1[0] * cz2) - })?; - - let (cm, r) = CM::commit(&pk.ck, &[w, &t[..]].concat(), rng)?; - - let rho_bits = { - transcript.add(&U); - transcript.add(&u); - transcript.add(&cm); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - - Ok(( - RW { - w: cfg_iter!(W.w) - .zip(&w[..]) - .map(|(a, b)| rho * b + a) - .collect(), - r: W.r + r * rho, - }, - RU { - u: U.u + rho, - cm: U.cm + cm * rho, - x: cfg_iter!(U.x) - .zip(&u[..]) - .map(|(a, b)| rho * b + a) - .collect(), - }, - cm, - rho_bits.try_into().unwrap(), - )) - } -} - -impl - FoldingSchemeVerifier<1, 1> for AbstractOva -{ - #[allow(non_snake_case)] - fn verify( - _vk: &(), - transcript: &mut impl Transcript, - Us: &[impl Borrow; 1], - us: &[impl Borrow; 1], - cm: &Self::Proof<1, 1>, - ) -> Result { - let (U, u) = (Us[0].borrow(), us[0].borrow()); - - let rho_bits = { - transcript.add(&U); - transcript.add(&u); - transcript.add(cm); - transcript.challenge_bits(CHALLENGE_BITS) - }; - let rho = CM::Scalar::from_bits_le(&rho_bits); - - Ok(RU { - u: U.u + rho, - cm: U.cm + *cm * rho, - x: cfg_iter!(U.x) - .zip(&u[..]) - .map(|(a, b)| rho * b + a) - .collect(), - }) - } -} - pub struct AbstractOvaGadget { _t: PhantomData, } @@ -307,85 +181,6 @@ where type Proof = CM::CommitmentVar; } -impl FoldingSchemePartialVerifierGadget<1, 1> - for AbstractOvaGadget -where - CM: CommitmentDefGadget, -{ - #[allow(non_snake_case)] - fn verify_hinted( - _vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - [U]: [&Self::RU; 1], - [u]: [&Self::IU; 1], - proof: &Self::Proof<1, 1>, - ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { - let rho_bits = { - transcript.add(&U)?; - transcript.add(&u)?; - transcript.add(proof)?; - transcript.challenge_bits(CHALLENGE_BITS)? - }; - let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; - - Ok(( - Self::RU { - u: (U.u.clone() + &rho) - .try_into() - .map_err(|_| SynthesisError::Unsatisfiable)?, - cm: CM::CommitmentVar::new_witness(U.cm.cs().or(proof.cs()).or(rho.cs()), || { - Ok(U.cm.value().unwrap_or_default() - + proof.value().unwrap_or_default() * rho.value().unwrap_or_default()) - })?, - x: U.x - .iter() - .zip(&u[..]) - .map(|(a, b)| (b.clone() * &rho + a).try_into()) - .collect::>() - .map_err(|_| SynthesisError::Unsatisfiable)?, - }, - rho_bits.try_into().unwrap(), - )) - } -} - -impl FoldingSchemeFullVerifierGadget<1, 1> - for AbstractOvaGadget -where - CM: CommitmentDefGadget, - CM::CommitmentVar: CurveVar<::Commitment, CM::ConstraintField>, -{ - #[allow(non_snake_case)] - fn verify( - _vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - [U]: [&Self::RU; 1], - [u]: [&Self::IU; 1], - proof: &Self::Proof<1, 1>, - ) -> Result { - let rho_bits = { - transcript.add(&U)?; - transcript.add(&u)?; - transcript.add(proof)?; - transcript.challenge_bits(CHALLENGE_BITS)? - }; - let rho = CM::ScalarVar::from_bits_le(&rho_bits)?; - - Ok(Self::RU { - u: (U.u.clone() + &rho) - .try_into() - .map_err(|_| SynthesisError::Unsatisfiable)?, - cm: proof.scalar_mul_le(rho_bits.iter())? + &U.cm, - x: U.x - .iter() - .zip(&u[..]) - .map(|(a, b)| (b.clone() * &rho + a).try_into()) - .collect::>() - .map_err(|_| SynthesisError::Unsatisfiable)?, - }) - } -} - impl GroupBasedFoldingSchemePrimaryDef for AbstractOva { diff --git a/crates/fs/src/ova/witness/circuits.rs b/crates/fs/src/ova/witnesses/circuits.rs similarity index 100% rename from crates/fs/src/ova/witness/circuits.rs rename to crates/fs/src/ova/witnesses/circuits.rs diff --git a/crates/fs/src/ova/witness/mod.rs b/crates/fs/src/ova/witnesses/mod.rs similarity index 100% rename from crates/fs/src/ova/witness/mod.rs rename to crates/fs/src/ova/witnesses/mod.rs diff --git a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs index 25f6dacd7..d1443099f 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs @@ -60,6 +60,7 @@ impl FoldingSchemeCycleFo type CFCircuit = OvaCycleFoldCircuit; + #[allow(non_snake_case)] fn to_cyclefold_circuits( [U]: &[impl Borrow; 1], _us: &[impl Borrow; 1], @@ -72,6 +73,7 @@ impl FoldingSchemeCycleFo }] } + #[allow(non_snake_case)] fn to_cyclefold_inputs( [U]: [::RU; 1], _us: [::IU; 1], From 96e3b7b409e3c99655164e6caad0ee24467ee55e Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 18:35:53 +0800 Subject: [PATCH 87/93] Actually test wasm targets --- crates/fs/src/ova/mod.rs | 6 ++++-- crates/ivc/src/compilers/cyclefold/adapters/ova.rs | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs index 1b90d37ea..5a9b5c44c 100644 --- a/crates/fs/src/ova/mod.rs +++ b/crates/fs/src/ova/mod.rs @@ -197,11 +197,13 @@ impl GroupBasedFoldingSch mod tests { use ark_bn254::{Fq, Fr, G1Projective}; use ark_ff::UniformRand; - use ark_std::{error::Error, test_rng}; + use ark_std::{error::Error, rand::thread_rng}; use sonobe_primitives::{ circuits::utils::{CircuitForTest, satisfying_assignments_for_test}, commitments::pedersen::Pedersen, }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::tests::test_folding_scheme; @@ -238,7 +240,7 @@ mod tests { #[test] fn test_ova() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); test_ova_opt::(10, &mut rng)?; test_ova_opt::(10, &mut rng)?; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs index d1443099f..6c9d495de 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs @@ -114,6 +114,8 @@ mod tests { commitments::pedersen::Pedersen, transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::tests::test_ivc; From 0c0c9e1f10f5b5a05ee0226b59ae29172cc28ecc Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:10:33 +0800 Subject: [PATCH 88/93] Add Ova docs --- crates/fs/src/ova/algorithms/key_generator.rs | 2 ++ crates/fs/src/ova/algorithms/mod.rs | 2 ++ crates/fs/src/ova/algorithms/preprocessor.rs | 2 ++ crates/fs/src/ova/algorithms/prover.rs | 2 ++ crates/fs/src/ova/algorithms/verifier.rs | 2 ++ crates/fs/src/ova/circuits/mod.rs | 2 ++ crates/fs/src/ova/circuits/verifier.rs | 2 ++ crates/fs/src/ova/instances/circuits.rs | 8 ++++++++ crates/fs/src/ova/instances/mod.rs | 8 ++++++++ crates/fs/src/ova/mod.rs | 14 ++++++++++++-- crates/fs/src/ova/witnesses/circuits.rs | 5 +++++ crates/fs/src/ova/witnesses/mod.rs | 6 ++++++ crates/ivc/src/compilers/cyclefold/adapters/ova.rs | 8 +++++++- 13 files changed, 60 insertions(+), 3 deletions(-) diff --git a/crates/fs/src/ova/algorithms/key_generator.rs b/crates/fs/src/ova/algorithms/key_generator.rs index 7cfff3f3d..f897b3466 100644 --- a/crates/fs/src/ova/algorithms/key_generator.rs +++ b/crates/fs/src/ova/algorithms/key_generator.rs @@ -1,3 +1,5 @@ +//! Key generation for Ova. + use ark_std::sync::Arc; use sonobe_primitives::{ arithmetizations::{Arith, ArithConfig}, diff --git a/crates/fs/src/ova/algorithms/mod.rs b/crates/fs/src/ova/algorithms/mod.rs index 11e838298..7c51fe3d8 100644 --- a/crates/fs/src/ova/algorithms/mod.rs +++ b/crates/fs/src/ova/algorithms/mod.rs @@ -1,3 +1,5 @@ +//! Implementations folding scheme algorithms for Ova. + pub mod key_generator; pub mod preprocessor; pub mod prover; diff --git a/crates/fs/src/ova/algorithms/preprocessor.rs b/crates/fs/src/ova/algorithms/preprocessor.rs index d5e988c91..36729fb56 100644 --- a/crates/fs/src/ova/algorithms/preprocessor.rs +++ b/crates/fs/src/ova/algorithms/preprocessor.rs @@ -1,3 +1,5 @@ +//! Preprocessing for Ova. + use ark_std::rand::RngCore; use sonobe_primitives::{commitments::GroupBasedCommitment, traits::SonobeField}; diff --git a/crates/fs/src/ova/algorithms/prover.rs b/crates/fs/src/ova/algorithms/prover.rs index 1941951bd..12fc69f94 100644 --- a/crates/fs/src/ova/algorithms/prover.rs +++ b/crates/fs/src/ova/algorithms/prover.rs @@ -1,3 +1,5 @@ +//! Proof generation for Ova. + use ark_ff::One; use ark_std::{borrow::Borrow, cfg_iter, rand::RngCore}; #[cfg(feature = "parallel")] diff --git a/crates/fs/src/ova/algorithms/verifier.rs b/crates/fs/src/ova/algorithms/verifier.rs index 57f771fe3..72754dd9e 100644 --- a/crates/fs/src/ova/algorithms/verifier.rs +++ b/crates/fs/src/ova/algorithms/verifier.rs @@ -1,3 +1,5 @@ +//! Proof verification for Ova. + use ark_std::{borrow::Borrow, cfg_iter}; #[cfg(feature = "parallel")] use rayon::prelude::*; diff --git a/crates/fs/src/ova/circuits/mod.rs b/crates/fs/src/ova/circuits/mod.rs index 9a0722027..ffed2641a 100644 --- a/crates/fs/src/ova/circuits/mod.rs +++ b/crates/fs/src/ova/circuits/mod.rs @@ -1 +1,3 @@ +//! In-circuit gadgets for Ova. + pub mod verifier; diff --git a/crates/fs/src/ova/circuits/verifier.rs b/crates/fs/src/ova/circuits/verifier.rs index eb397a597..8581e809d 100644 --- a/crates/fs/src/ova/circuits/verifier.rs +++ b/crates/fs/src/ova/circuits/verifier.rs @@ -1,3 +1,5 @@ +//! Partial and full in-circuit verifier implementations for Ova. + use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, groups::CurveVar}; use ark_relations::gr1cs::SynthesisError; use sonobe_primitives::{ diff --git a/crates/fs/src/ova/instances/circuits.rs b/crates/fs/src/ova/instances/circuits.rs index 06af66ff5..3b3ee1a29 100644 --- a/crates/fs/src/ova/instances/circuits.rs +++ b/crates/fs/src/ova/instances/circuits.rs @@ -1,3 +1,5 @@ +//! In-circuit variables for Ova instances. + use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, @@ -12,10 +14,16 @@ use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::Absorbabl use super::RunningInstance; use crate::FoldingInstanceVar; +/// [`RunningInstanceVar`] defines Ova's running instance variable. #[derive(Clone, Debug, PartialEq)] pub struct RunningInstanceVar { + /// [`RunningInstanceVar::u`] is the constant term. pub u: CM::ScalarVar, + /// [`RunningInstanceVar::cm`] is the combined witness and error term + /// commitment. pub cm: CM::CommitmentVar, + /// [`RunningInstanceVar::x`] is the vector of public inputs (to the + /// circuit). pub x: Vec, } diff --git a/crates/fs/src/ova/instances/mod.rs b/crates/fs/src/ova/instances/mod.rs index 283057bf8..498373588 100644 --- a/crates/fs/src/ova/instances/mod.rs +++ b/crates/fs/src/ova/instances/mod.rs @@ -1,3 +1,6 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Ova +//! instances. + use ark_ff::PrimeField; use sonobe_primitives::{ arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, @@ -8,10 +11,15 @@ use crate::FoldingInstance; pub mod circuits; +/// [`RunningInstance`] defines Ova's running instance. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningInstance { + /// [`RunningInstance::u`] is the constant term. pub u: CM::Scalar, + /// [`RunningInstance::cm`] is the combined witness and error term + /// commitment. pub cm: CM::Commitment, + /// [`RunningInstance::x`] is the vector of public inputs (to the circuit). pub x: Vec, } diff --git a/crates/fs/src/ova/mod.rs b/crates/fs/src/ova/mod.rs index 5a9b5c44c..f9299091c 100644 --- a/crates/fs/src/ova/mod.rs +++ b/crates/fs/src/ova/mod.rs @@ -1,7 +1,10 @@ +//! This module implements the Ova folding scheme, which is introduced in this +//! [note]. +//! +//! [note]: https://hackmd.io/V4838nnlRKal9ZiTHiGYzw + use ark_r1cs_std::boolean::Boolean; use ark_std::{UniformRand, marker::PhantomData, rand::RngCore, sync::Arc}; -#[cfg(feature = "parallel")] -use rayon::prelude::*; use sonobe_primitives::{ arithmetizations::{ Arith, ArithConfig, ArithRelation, @@ -28,6 +31,7 @@ pub mod circuits; pub mod instances; pub mod witnesses; +/// [`OvaKey`] is Ova's decider key. #[derive(Clone)] pub struct OvaKey { arith: Arc, @@ -133,13 +137,18 @@ where } } +/// [`AbstractOva`] implements the Ova folding scheme which can operate on +/// both the primary and secondary curves. pub struct AbstractOva { _t: PhantomData<(CM, TF)>, } +/// [`Ova`] is the main Ova folding scheme on the primary curve. pub type Ova = AbstractOva::Scalar, CHALLENGE_BITS>; +/// [`CycleFoldOva`] is the Ova folding scheme on the secondary curve which can +/// be used as the folding scheme for folding CycleFold instances. pub type CycleFoldOva = AbstractOva::Commitment>, CHALLENGE_BITS>; @@ -162,6 +171,7 @@ impl Fol type Proof = CM::Commitment; } +/// [`AbstractOvaGadget`] is the in-circuit gadget for [`AbstractOva`]. pub struct AbstractOvaGadget { _t: PhantomData, } diff --git a/crates/fs/src/ova/witnesses/circuits.rs b/crates/fs/src/ova/witnesses/circuits.rs index 0a7652fc6..515c22a29 100644 --- a/crates/fs/src/ova/witnesses/circuits.rs +++ b/crates/fs/src/ova/witnesses/circuits.rs @@ -1,3 +1,5 @@ +//! In-circuit variables for Ova witnesses. + use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, @@ -8,9 +10,12 @@ use sonobe_primitives::commitments::CommitmentDefGadget; use super::RunningWitness; +/// [`RunningWitnessVar`] defines Ova's running witness variable. #[derive(Debug, PartialEq)] pub struct RunningWitnessVar { + /// [`RunningWitnessVar::w`] is the witness (to the circuit). pub w: Vec, + /// [`RunningWitnessVar::r`] is the randomness for the witness commitment. pub r: CM::RandomnessVar, } diff --git a/crates/fs/src/ova/witnesses/mod.rs b/crates/fs/src/ova/witnesses/mod.rs index a20c1a854..7fefbf852 100644 --- a/crates/fs/src/ova/witnesses/mod.rs +++ b/crates/fs/src/ova/witnesses/mod.rs @@ -1,12 +1,18 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Ova +//! witnesses. + use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; use crate::FoldingWitness; pub mod circuits; +/// [`RunningWitness`] defines Ova's running witness. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningWitness { + /// [`RunningWitness::w`] is the witness (to the circuit). pub w: Vec, + /// [`RunningWitness::r`] is the randomness for the witness commitment. pub r: CM::Randomness, } diff --git a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs index 6c9d495de..6fb0e5d46 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/ova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/ova.rs @@ -1,3 +1,5 @@ +//! Ova CycleFold adapter that bridges Ova into the CycleFold IVC compiler. + use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean}; use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; @@ -20,7 +22,7 @@ use crate::compilers::cyclefold::{ CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, }; -/// Configuration for Ova's CycleFold circuit +/// [`OvaCycleFoldCircuit`] defines CycleFold circuit for Ova. pub struct OvaCycleFoldCircuit { r: Vec, points: Vec, @@ -97,9 +99,13 @@ impl FoldingSchemeCycleFo } } +/// [`OvaOvaIVC`] defines a CycleFold-based IVC using Ova as the primary folding +/// scheme and Ova as the secondary folding scheme. pub type OvaOvaIVC = CycleFoldBasedIVC, CycleFoldOva, T>; +/// [`OvaNovaIVC`] defines a CycleFold-based IVC using Ova as the primary +/// folding scheme and Nova as the secondary folding scheme. pub type OvaNovaIVC = CycleFoldBasedIVC, CycleFoldNova, T>; From 126416d29766846d2ff93221473bd781d08da11d Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 18 Nov 2025 04:30:45 +0800 Subject: [PATCH 89/93] Refactor: start porting Mova --- crates/fs/src/lib.rs | 1 + crates/fs/src/mova/instance/mod.rs | 56 ++++ crates/fs/src/mova/mod.rs | 421 +++++++++++++++++++++++++++++ crates/fs/src/mova/witness/mod.rs | 30 ++ 4 files changed, 508 insertions(+) create mode 100644 crates/fs/src/mova/instance/mod.rs create mode 100644 crates/fs/src/mova/mod.rs create mode 100644 crates/fs/src/mova/witness/mod.rs diff --git a/crates/fs/src/lib.rs b/crates/fs/src/lib.rs index 1c0f0b8fb..582dd98d1 100644 --- a/crates/fs/src/lib.rs +++ b/crates/fs/src/lib.rs @@ -25,6 +25,7 @@ //! - `witnesses/`: Witness types. pub mod definitions; +pub mod mova; pub mod nova; pub mod ova; diff --git a/crates/fs/src/mova/instance/mod.rs b/crates/fs/src/mova/instance/mod.rs new file mode 100644 index 000000000..65d1250e9 --- /dev/null +++ b/crates/fs/src/mova/instance/mod.rs @@ -0,0 +1,56 @@ +use ark_ff::PrimeField; +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, + transcripts::Absorbable, +}; + +use crate::FoldingInstance; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningInstance { + // Random evaluation point for the E + pub r_e: Vec, + // Evaluation of the MLE of E at r_E + pub v: CM::Scalar, + pub u: CM::Scalar, + pub cm_w: CM::Commitment, + pub x: Vec, +} + +impl FoldingInstance for RunningInstance { + const N_COMMITMENTS: usize = 1; + + fn commitments(&self) -> Vec<&CM::Commitment> { + vec![&self.cm_w] + } + + fn public_inputs(&self) -> &[CM::Scalar] { + &self.x + } + + fn public_inputs_mut(&mut self) -> &mut [CM::Scalar] { + &mut self.x + } +} + +impl Dummy<&Cfg> for RunningInstance { + fn dummy(cfg: &Cfg) -> Self { + Self { + r_e: vec![Default::default(); cfg.log_constraints()], + v: Default::default(), + u: Default::default(), + cm_w: Default::default(), + x: vec![Default::default(); cfg.n_public_inputs()], + } + } +} + +impl Absorbable for RunningInstance { + fn absorb_into(&self, dest: &mut Vec) { + self.u.absorb_into(dest); + self.x.absorb_into(dest); + self.v.absorb_into(dest); + self.r_e.absorb_into(dest); + self.cm_w.absorb_into(dest); + } +} diff --git a/crates/fs/src/mova/mod.rs b/crates/fs/src/mova/mod.rs new file mode 100644 index 000000000..31bc3f5d7 --- /dev/null +++ b/crates/fs/src/mova/mod.rs @@ -0,0 +1,421 @@ +use ark_ff::{Field, One, Zero}; +use ark_poly::{ + DenseMultilinearExtension as MLE, DenseUVPolynomial, Polynomial, univariate::DensePolynomial, +}; +use ark_std::{ + UniformRand, borrow::Borrow, cfg_into_iter, cfg_iter, marker::PhantomData, rand::RngCore, + sync::Arc, +}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::{bits::FromBits, poly::MLEHelper}, + arithmetizations::{ + Arith, ArithConfig, ArithRelation, + r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, + }, + circuits::AssignmentsOwned, + commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, + relations::{Relation, WitnessInstanceSampler}, + traits::{CF1, Dummy, SonobeCurve}, + transcripts::Transcript, +}; + +use self::{instance::RunningInstance as RU, witness::RunningWitness as RW}; +use crate::{ + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeKeyGenerator, FoldingSchemePreprocessor, + FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as IU, PlainWitness as IW, +}; + +pub mod instance; +pub mod witness; + +#[derive(Clone)] +pub struct MovaKey { + pub arith: Arc, + pub ck: Arc, +} + +impl DeciderKey for MovaKey { + type ProverKey = Self; + type VerifierKey = (); + type ArithConfig = A::Config; + + fn to_pk(&self) -> &Self::ProverKey { + self + } + + fn to_vk(&self) -> &Self::VerifierKey { + &() + } + + fn to_arith_config(&self) -> &Self::ArithConfig { + self.arith.config() + } +} + +impl Relation, RU> for MovaKey +where + A: for<'a> ArithRelation< + RelaxedWitness<&'a [CM::Scalar]>, + RelaxedInstance<&'a [CM::Scalar]>, + Evaluation = Vec, + >, + CM: CommitmentOps, +{ + type Error = Error; + + fn check_relation(&self, w: &RW, u: &RU) -> Result<(), Self::Error> { + self.arith.check_relation( + &RelaxedWitness { w: &w.w, e: &w.e }, + &RelaxedInstance { x: &u.x, u: &u.u }, + )?; + CM::open(&self.ck, &w.w, &w.r_w, &u.cm_w)?; + + (MLE::from_evaluations(&w.e).evaluate(&u.r_e) == u.v) + .then_some(()) + .ok_or_else(|| { + Error::UnsatisfiedRelation("Error term does not evaluate to claimed value".into()) + }) + } +} + +impl Relation, IU> for MovaKey +where + A: ArithRelation, Vec>, + CM: CommitmentDef, +{ + type Error = Error; + + fn check_relation(&self, w: &IW, u: &IU) -> Result<(), Self::Error> { + self.arith.check_relation(w, u)?; + Ok(()) + } +} + +impl WitnessInstanceSampler, IU> + for MovaKey +{ + type Source = AssignmentsOwned; + type Error = Error; + + fn sample( + &self, + z: Self::Source, + _rng: impl RngCore, + ) -> Result<(IW, IU), Error> { + Ok((z.private.into(), z.public.into())) + } +} + +impl WitnessInstanceSampler, RU> for MovaKey +where + A: for<'a> ArithRelation< + RelaxedWitness<&'a [CM::Scalar]>, + RelaxedInstance<&'a [CM::Scalar]>, + Evaluation = Vec, + >, + CM: CommitmentOps, +{ + type Source = (); + type Error = Error; + + fn sample(&self, _: Self::Source, mut rng: impl RngCore) -> Result<(RW, RU), Error> { + let cfg = self.arith.config(); + + let u = CM::Scalar::rand(&mut rng); + let x = (0..cfg.n_public_inputs()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let w = (0..cfg.n_witnesses()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let e = self.arith.eval_relation( + &RelaxedWitness { w: &w, e: &[] }, + &RelaxedInstance { x: &x, u: &u }, + )?; + + let (cm_w, r_w) = CM::commit(&self.ck, &w, &mut rng)?; + + let r_e = (0..cfg.log_constraints()) + .map(|_| CM::Scalar::rand(&mut rng)) + .collect::>(); + let v = MLE::from_evaluations(&e).evaluate(&r_e); + + Ok((RW { w, r_w, e }, RU { x, cm_w, u, r_e, v })) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MovaProof { + pub h1_coeffs: Vec>, + pub t: CF1, + pub cm_w: C, +} + +impl Dummy<&Cfg> for MovaProof { + fn dummy(cfg: &Cfg) -> Self { + Self { + h1_coeffs: vec![Zero::zero(); cfg.log_constraints()], + t: Zero::zero(), + cm_w: Zero::zero(), + } + } +} + +pub struct Mova { + _vc: PhantomData, +} + +impl FoldingSchemeDef + for Mova +{ + type CM = CM; + type RW = RW; + type RU = RU; + type IW = IW; + type IU = IU; + + type TranscriptField = CM::Scalar; + type Arith = R1CS; + + type Config = usize; + type PublicParam = CM::Key; + type DeciderKey = MovaKey; + type Challenge = [bool; CHALLENGE_BITS]; + type Proof = MovaProof; +} + +impl FoldingSchemePreprocessor + for Mova +{ + fn preprocess(n_witnesses: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(n_witnesses, &mut rng)?; + Ok(ck) + } +} + +impl FoldingSchemeKeyGenerator + for Mova +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + if ck.max_scalars_len() < r1cs.config().n_witnesses() { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the R1CS instance".into(), + )); + } + Ok(MovaKey { arith: r1cs, ck }) + } +} + +impl FoldingSchemeProver<1, 1> + for Mova +{ + #[allow(non_snake_case)] + fn prove( + pk: &MovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Protocol 5 + + // Step 5.1: Commit to w & send commitment + let (cm_w, r_w) = CM::commit(&pk.ck, w, rng)?; + + transcript.add(U); + transcript.add(u); + transcript.add(&cm_w); + + // Step 5.2: Get challenge r_E + let r_e = transcript.challenge_field_elements(U.r_e.len()); + + // Protocol 6 + + // Step 6.1: Compute l(X) such that l(0) = r1, l(1) = r2 + let l = U + .r_e + .iter() + .zip(&r_e) + .map(|(&r1, &r2)| DensePolynomial::from_coefficients_vec(vec![r1, r2 - r1])) + .collect::>(); + // Step 6.1: Compute h1(X) and h2(X), where h2(X) is empty in our case + let h1 = { + // Initialize the polynomial vector from the evaluations in the MLE. + // Each evaluation is turned into a constant polynomial. + let mut poly = + W.e.iter() + .chain(vec![Zero::zero(); 1 << U.r_e.len()].iter()) + .map(|&x| DensePolynomial::from_coefficients_slice(&[x])) + .collect::>(); + + for i in &l { + poly = poly + .chunks_exact(2) + .map(|w| &w[0] + (&w[1] - &w[0]).naive_mul(i)) + .collect(); + } + + poly.swap_remove(0) + }; + // Step 6.1: Send h1(X) and h2(X), where the constant term is omitted + // because it always equals v + let mut h1_coeffs = h1.coeffs.clone(); + h1_coeffs.resize(pk.arith.config().log_constraints() + 1, Zero::zero()); + h1_coeffs.remove(0); + transcript.add(&h1_coeffs); + + // Step 6.2: Get challenge beta + let beta = transcript.challenge_field_element(); + + // Step 6.3: Compute r_E' + let r_e_prime = l.iter().map(|i| i.evaluate(&beta)).collect(); + + // Protocol 7 + + // Step 7.1: Compute cross term `T`. We follow the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u[..]).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w[..]).map(|(a, b)| *a + b).collect(), + )))?; + let T = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + // Step 7.1: Evaluate & send T's MLE at r_E' + let t = MLE::from_evaluations(&T).evaluate(&r_e_prime); + transcript.add(&t); + + // Step 7.2: Get challenge rho + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + // Step 7.3: Compute new W and U + Ok(( + Self::RW { + e: cfg_iter!(W.e).zip(&T).map(|(a, b)| rho * b + a).collect(), + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W.r_w + r_w * rho, + }, + Self::RU { + r_e: r_e_prime, + v: h1.evaluate(&beta) + rho * t, + u: U.u + rho, + cm_w: U.cm_w + cm_w * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + MovaProof { h1_coeffs, t, cm_w }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl FoldingSchemeVerifier<1, 1> + for Mova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let h1 = DensePolynomial::from_coefficients_vec([&[U.v][..], &proof.h1_coeffs].concat()); + + transcript.add(U); + transcript.add(u); + transcript.add(&proof.cm_w); + + let r_e = transcript.challenge_field_elements(U.r_e.len()); + + transcript.add(&proof.h1_coeffs); + + let beta = transcript.challenge_field_element(); + + transcript.add(&proof.t); + + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(Self::RU { + r_e: U + .r_e + .iter() + .zip(r_e) + .map(|(&r1, r2)| r1 + beta * (r2 - r1)) + .collect(), + v: h1.evaluate(&beta) + rho * proof.t, + u: U.u + rho, + cm_w: U.cm_w + proof.cm_w * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective}; + use ark_ff::UniformRand; + use ark_std::{error::Error, rand::Rng, test_rng}; + use sonobe_primitives::{ + circuits::utils::{CircuitForTest, satisfying_assignments_for_test}, + commitments::pedersen::Pedersen, + }; + + use super::*; + use crate::tests::test_folding_scheme; + + fn test_mova_opt(rounds: usize, mut rng: impl Rng) -> Result<(), Box> { + test_folding_scheme::>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + + test_folding_scheme::>, 1, 1>( + 8, + CircuitForTest { + x: Fr::rand(&mut rng), + }, + (0..rounds) + .map(|_| satisfying_assignments_for_test(Fr::rand(&mut rng))) + .collect(), + &mut rng, + )?; + Ok(()) + } + + #[test] + fn test_mova() -> Result<(), Box> { + let mut rng = test_rng(); + test_mova_opt(10, &mut rng)?; + Ok(()) + } +} diff --git a/crates/fs/src/mova/witness/mod.rs b/crates/fs/src/mova/witness/mod.rs new file mode 100644 index 000000000..5b380881c --- /dev/null +++ b/crates/fs/src/mova/witness/mod.rs @@ -0,0 +1,30 @@ +use sonobe_primitives::{ + arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, +}; + +use crate::FoldingWitness; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RunningWitness { + pub w: Vec, + pub r_w: CM::Randomness, + pub e: Vec, +} + +impl FoldingWitness for RunningWitness { + const N_OPENINGS: usize = 1; + + fn openings(&self) -> Vec<(&[CM::Scalar], &CM::Randomness)> { + vec![(&self.w, &self.r_w)] + } +} + +impl Dummy<&Cfg> for RunningWitness { + fn dummy(cfg: &Cfg) -> Self { + Self { + w: vec![Default::default(); cfg.n_witnesses()], + r_w: Default::default(), + e: vec![Default::default(); cfg.n_constraints()], + } + } +} From 3688ecd71e3c765fb35fc1d4008111be1b3a5c91 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 20 Nov 2025 05:46:39 +0800 Subject: [PATCH 90/93] Circuits and CF adapter for Mova --- crates/fs/src/mova/instance/circuits.rs | 138 ++++++++++++++++ crates/fs/src/mova/instance/mod.rs | 2 + crates/fs/src/mova/mod.rs | 148 ++++++++++++++++- crates/fs/src/mova/witness/circuits.rs | 51 ++++++ crates/fs/src/mova/witness/mod.rs | 2 + .../src/compilers/cyclefold/adapters/mod.rs | 1 + .../src/compilers/cyclefold/adapters/mova.rs | 152 ++++++++++++++++++ 7 files changed, 487 insertions(+), 7 deletions(-) create mode 100644 crates/fs/src/mova/instance/circuits.rs create mode 100644 crates/fs/src/mova/witness/circuits.rs create mode 100644 crates/ivc/src/compilers/cyclefold/adapters/mova.rs diff --git a/crates/fs/src/mova/instance/circuits.rs b/crates/fs/src/mova/instance/circuits.rs new file mode 100644 index 000000000..4d6addc82 --- /dev/null +++ b/crates/fs/src/mova/instance/circuits.rs @@ -0,0 +1,138 @@ +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + fields::fp::FpVar, + select::CondSelectGadget, + GR1CSVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::AbsorbableVar}; + +use super::RunningInstance; +use crate::FoldingInstanceVar; + +#[derive(Clone, Debug, PartialEq)] +pub struct RunningInstanceVar { + // Random evaluation point for the E + pub r_e: Vec, + // Evaluation of the MLE of E at r_E + pub v: CM::ScalarVar, + pub u: CM::ScalarVar, + pub cm_w: CM::CommitmentVar, + pub x: Vec, +} + +impl AllocVar, CM::ConstraintField> + for RunningInstanceVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningInstance { r_e, v, u, cm_w, x } = v.borrow(); + Ok(Self { + r_e: AllocVar::new_variable(cs.clone(), || Ok(&r_e[..]), mode)?, + v: AllocVar::new_variable(cs.clone(), || Ok(v), mode)?, + u: AllocVar::new_variable(cs.clone(), || Ok(u), mode)?, + cm_w: AllocVar::new_variable(cs.clone(), || Ok(cm_w), mode)?, + x: AllocVar::new_variable(cs.clone(), || Ok(&x[..]), mode)?, + }) + } +} + +impl GR1CSVar for RunningInstanceVar { + type Value = RunningInstance; + + fn cs(&self) -> ConstraintSystemRef { + self.r_e + .cs() + .or(self.v.cs()) + .or(self.u.cs()) + .or(self.cm_w.cs()) + .or(self.x.cs()) + } + + fn value(&self) -> Result { + Ok(RunningInstance { + r_e: self.r_e.value()?, + v: self.v.value()?, + u: self.u.value()?, + cm_w: self.cm_w.value()?, + x: self.x.value()?, + }) + } +} + +impl AbsorbableVar for RunningInstanceVar { + fn absorb_into( + &self, + dest: &mut Vec>, + ) -> Result<(), SynthesisError> { + self.u.absorb_into(dest)?; + self.x.absorb_into(dest)?; + self.v.absorb_into(dest)?; + self.r_e.absorb_into(dest)?; + self.cm_w.absorb_into(dest) + } +} + +impl CondSelectGadget for RunningInstanceVar { + fn conditionally_select( + cond: &Boolean, + true_value: &Self, + false_value: &Self, + ) -> Result { + if true_value.r_e.len() != false_value.r_e.len() { + return Err(SynthesisError::Unsatisfiable); + } + if true_value.x.len() != false_value.x.len() { + return Err(SynthesisError::Unsatisfiable); + } + Ok(Self { + r_e: true_value + .r_e + .iter() + .zip(&false_value.r_e) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + v: cond.select(&true_value.v, &false_value.v)?, + u: cond.select(&true_value.u, &false_value.u)?, + x: true_value + .x + .iter() + .zip(&false_value.x) + .map(|(t, f)| cond.select(t, f)) + .collect::>()?, + cm_w: cond.select(&true_value.cm_w, &false_value.cm_w)?, + }) + } +} + +impl FoldingInstanceVar for RunningInstanceVar { + fn commitments(&self) -> Vec<&CM::CommitmentVar> { + vec![&self.cm_w] + } + + fn public_inputs(&self) -> &Vec { + &self.x + } + + fn new_witness_with_public_inputs( + cs: impl Into>, + u: &Self::Value, + x: Vec, + ) -> Result { + let cs = cs.into().cs(); + Ok(Self { + r_e: AllocVar::new_witness(cs.clone(), || Ok(&u.r_e[..]))?, + v: AllocVar::new_witness(cs.clone(), || Ok(u.v))?, + u: AllocVar::new_witness(cs.clone(), || Ok(u.u))?, + cm_w: AllocVar::new_witness(cs.clone(), || Ok(&u.cm_w))?, + x, + }) + } +} diff --git a/crates/fs/src/mova/instance/mod.rs b/crates/fs/src/mova/instance/mod.rs index 65d1250e9..a084d62c3 100644 --- a/crates/fs/src/mova/instance/mod.rs +++ b/crates/fs/src/mova/instance/mod.rs @@ -6,6 +6,8 @@ use sonobe_primitives::{ use crate::FoldingInstance; +pub mod circuits; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningInstance { // Random evaluation point for the E diff --git a/crates/fs/src/mova/mod.rs b/crates/fs/src/mova/mod.rs index 31bc3f5d7..144f1ba15 100644 --- a/crates/fs/src/mova/mod.rs +++ b/crates/fs/src/mova/mod.rs @@ -2,6 +2,14 @@ use ark_ff::{Field, One, Zero}; use ark_poly::{ DenseMultilinearExtension as MLE, DenseUVPolynomial, Polynomial, univariate::DensePolynomial, }; +use ark_r1cs_std::{ + GR1CSVar, + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + fields::fp::FpVar, + poly::polynomial::univariate::dense::DensePolynomialVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{ UniformRand, borrow::Borrow, cfg_into_iter, cfg_iter, marker::PhantomData, rand::RngCore, sync::Arc, @@ -9,22 +17,32 @@ use ark_std::{ #[cfg(feature = "parallel")] use rayon::prelude::*; use sonobe_primitives::{ - algebra::ops::{bits::FromBits, poly::MLEHelper}, + algebra::ops::{ + bits::{FromBits, FromBitsGadget}, + poly::MLEHelper, + }, arithmetizations::{ Arith, ArithConfig, ArithRelation, r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, }, circuits::AssignmentsOwned, - commitments::{CommitmentDef, CommitmentKey, CommitmentOps, GroupBasedCommitment}, + commitments::{ + CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, GroupBasedCommitment, + }, relations::{Relation, WitnessInstanceSampler}, traits::{CF1, Dummy, SonobeCurve}, - transcripts::Transcript, + transcripts::{Transcript, TranscriptGadget}, }; -use self::{instance::RunningInstance as RU, witness::RunningWitness as RW}; +use self::{ + instance::{RunningInstance as RU, circuits::RunningInstanceVar as RUVar}, + witness::RunningWitness as RW, +}; use crate::{ - DeciderKey, Error, FoldingSchemeDef, FoldingSchemeKeyGenerator, FoldingSchemePreprocessor, - FoldingSchemeProver, FoldingSchemeVerifier, PlainInstance as IU, PlainWitness as IW, + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeKeyGenerator, + FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, FoldingSchemeProver, + FoldingSchemeVerifier, GroupBasedFoldingSchemePrimaryDef, PlainInstance as IU, + PlainInstanceVar as IUVar, PlainWitness as IW, }; pub mod instance; @@ -164,7 +182,7 @@ impl Dummy<&Cfg> for MovaProof { } pub struct Mova { - _vc: PhantomData, + _t: PhantomData, } impl FoldingSchemeDef @@ -374,6 +392,122 @@ impl FoldingSchemeVerifie } } +#[derive(Clone)] +pub struct MovaProofVar { + pub h1_coeffs: Vec>>, + pub t: FpVar>, + pub cm_w: C::EmulatedVar>, +} + +impl AllocVar, CF1> for MovaProofVar { + fn new_variable>>( + cs: impl Into>>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let ns = cs.into(); + let cs = ns.cs(); + + let proof = f()?.borrow().clone(); + + Ok(Self { + h1_coeffs: Vec::new_variable(cs.clone(), || Ok(&proof.h1_coeffs[..]), mode)?, + t: FpVar::new_variable(cs.clone(), || Ok(proof.t), mode)?, + cm_w: AllocVar::new_variable(cs.clone(), || Ok(proof.cm_w), mode)?, + }) + } +} + +impl GR1CSVar> for MovaProofVar { + type Value = MovaProof; + + fn cs(&self) -> ConstraintSystemRef> { + self.h1_coeffs.cs().or(self.t.cs()).or(self.cm_w.cs()) + } + + fn value(&self) -> Result { + Ok(MovaProof { + h1_coeffs: self.h1_coeffs.value()?, + t: self.t.value()?, + cm_w: self.cm_w.value()?, + }) + } +} + +pub struct MovaGadget { + _t: PhantomData, +} + +impl FoldingSchemeDefGadget + for MovaGadget +{ + type Widget = Mova; + + type CM = CM::Gadget2; + type RU = RUVar; + type IU = IUVar<::ScalarVar>; + type VerifierKey = (); + type Challenge = [Boolean; CHALLENGE_BITS]; + type Proof = MovaProofVar; +} + +impl FoldingSchemePartialVerifierGadget<1, 1> + for MovaGadget +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let h1 = DensePolynomialVar::from_coefficients_vec( + [&[U.v.clone()][..], &proof.h1_coeffs].concat(), + ); + + transcript.add(U)?; + transcript.add(u)?; + transcript.add(&proof.cm_w)?; + + let r_e = transcript.challenge_field_elements(U.r_e.len())?; + + transcript.add(&proof.h1_coeffs)?; + + let beta = transcript.challenge_field_element()?; + + transcript.add(&proof.t)?; + + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS)?; + let rho = FpVar::from_bits_le(&rho_bits)?; + + Ok(( + RUVar { + r_e: U + .r_e + .iter() + .zip(r_e) + .map(|(r1, r2)| r1 + &beta * (r2 - r1)) + .collect(), + v: h1.evaluate(&beta)? + &rho * &proof.t, + u: &U.u + &rho, + cm_w: AllocVar::new_witness(U.cm_w.cs().or(proof.cm_w.cs()).or(rho.cs()), || { + Ok(U.cm_w.value().unwrap_or_default() + + proof.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + })?, + x: U.x.iter().zip(&u[..]).map(|(a, b)| &rho * b + a).collect(), + }, + rho_bits.try_into().unwrap(), + )) + } +} + +impl GroupBasedFoldingSchemePrimaryDef + for Mova +{ + type Gadget = MovaGadget; +} + #[cfg(test)] mod tests { use ark_bn254::{Fr, G1Projective}; diff --git a/crates/fs/src/mova/witness/circuits.rs b/crates/fs/src/mova/witness/circuits.rs new file mode 100644 index 000000000..d0212f153 --- /dev/null +++ b/crates/fs/src/mova/witness/circuits.rs @@ -0,0 +1,51 @@ +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + GR1CSVar, +}; +use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +use sonobe_primitives::commitments::CommitmentDefGadget; + +use super::RunningWitness; + +#[derive(Debug, PartialEq)] +pub struct RunningWitnessVar { + pub w: Vec, + pub r_w: CM::RandomnessVar, + pub e: Vec, +} + +impl AllocVar, CM::ConstraintField> + for RunningWitnessVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + let cs = cs.into().cs(); + let v = f()?; + let RunningWitness { w, r_w, e } = v.borrow(); + Ok(Self { + w: AllocVar::new_variable(cs.clone(), || Ok(&w[..]), mode)?, + r_w: AllocVar::new_variable(cs.clone(), || Ok(r_w), mode)?, + e: AllocVar::new_variable(cs.clone(), || Ok(&e[..]), mode)?, + }) + } +} + +impl GR1CSVar for RunningWitnessVar { + type Value = RunningWitness; + + fn cs(&self) -> ConstraintSystemRef { + self.w.cs().or(self.r_w.cs()).or(self.e.cs()) + } + + fn value(&self) -> Result { + Ok(RunningWitness { + w: self.w.value()?, + r_w: self.r_w.value()?, + e: self.e.value()?, + }) + } +} diff --git a/crates/fs/src/mova/witness/mod.rs b/crates/fs/src/mova/witness/mod.rs index 5b380881c..4a54b7b45 100644 --- a/crates/fs/src/mova/witness/mod.rs +++ b/crates/fs/src/mova/witness/mod.rs @@ -4,6 +4,8 @@ use sonobe_primitives::{ use crate::FoldingWitness; +pub mod circuits; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningWitness { pub w: Vec, diff --git a/crates/ivc/src/compilers/cyclefold/adapters/mod.rs b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs index 8994ba2cd..6e628748d 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/mod.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/mod.rs @@ -1,5 +1,6 @@ //! Per-scheme adapters that implement [`super::CycleFoldCircuit`] for supported //! folding schemes. +pub mod mova; pub mod nova; pub mod ova; diff --git a/crates/ivc/src/compilers/cyclefold/adapters/mova.rs b/crates/ivc/src/compilers/cyclefold/adapters/mova.rs new file mode 100644 index 000000000..d358867e2 --- /dev/null +++ b/crates/ivc/src/compilers/cyclefold/adapters/mova.rs @@ -0,0 +1,152 @@ +use ark_ff::{PrimeField, Zero}; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean}; +use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::{borrow::Borrow, iter::once}; +use sonobe_fs::{FoldingSchemeDefGadget, mova::Mova, nova::CycleFoldNova, ova::CycleFoldOva}; +use sonobe_primitives::{ + algebra::{ + field::emulated::{Bounds, EmulatedFieldVar}, + ops::bits::{FromBits, FromBitsGadget, ToBitsGadgetExt}, + }, + commitments::GroupBasedCommitment, + traits::{CF2, SonobeCurve}, +}; + +use crate::compilers::cyclefold::{ + CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, +}; + +/// Configuration for Mova's CycleFold circuit +pub struct MovaCycleFoldCircuit { + r: Vec, + points: Vec, +} + +impl Default + for MovaCycleFoldCircuit +{ + fn default() -> Self { + Self { + r: vec![false; CHALLENGE_BITS], + points: vec![C::zero(); 2], + } + } +} + +impl CycleFoldCircuit> + for MovaCycleFoldCircuit +{ + fn verify_point_rlc(&self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let rho = FpVar::new_input(cs.clone(), || Ok(CF2::::from_bits_le(&self.r[..])))?; + let rho_bits = rho.to_n_bits_le(CHALLENGE_BITS)?; + + let points = Vec::::new_witness(cs.clone(), || Ok(&self.points[..]))?; + for point in &points { + Self::mark_point_as_public(point)?; + } + + Self::mark_point_as_public(&(points[1].scalar_mul_le(rho_bits.iter())? + &points[0])) + } +} + +impl FoldingSchemeCycleFoldExt<1, 1> + for Mova +{ + const N_CYCLEFOLDS: usize = 1; + + type CFCircuit = MovaCycleFoldCircuit; + + #[allow(non_snake_case)] + fn to_cyclefold_circuits( + [U]: &[impl Borrow; 1], + _us: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + rho: Self::Challenge, + ) -> Vec { + vec![MovaCycleFoldCircuit { + r: rho.into(), + points: vec![U.borrow().cm_w, proof.cm_w], + }] + } + + #[allow(non_snake_case)] + fn to_cyclefold_inputs( + [U]: [::RU; 1], + _us: [::IU; 1], + UU: ::RU, + proof: ::Proof<1, 1>, + rho: ::Challenge, + ) -> Result>>>, SynthesisError> { + let mut rho = rho.to_vec(); + rho.resize( + CF2::::MODULUS_BIT_SIZE as usize, + Boolean::FALSE, + ); + Ok(vec![ + once(EmulatedFieldVar::from_bounded_bits_le( + &rho, + Bounds(Zero::zero(), CF2::::MODULUS.into().into()), + )?) + .chain( + [U.cm_w, proof.cm_w, UU.cm_w] + .into_iter() + .flat_map(|p| [p.x, p.y]), + ) + .collect(), + ]) + } +} + +pub type MovaOvaIVC = + CycleFoldBasedIVC, CycleFoldOva, T>; + +pub type MovaNovaIVC = + CycleFoldBasedIVC, CycleFoldNova, T>; + +#[cfg(test)] +mod tests { + use ark_bn254::{Fr, G1Projective as C1}; + use ark_ff::UniformRand; + use ark_grumpkin::Projective as C2; + use ark_std::{error::Error, sync::Arc, test_rng}; + use sonobe_primitives::{ + circuits::utils::CircuitForTest, + commitments::pedersen::Pedersen, + transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, + }; + + use super::*; + use crate::tests::test_ivc; + + #[test] + fn test_mova_ova() -> Result<(), Box> { + let mut rng = test_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + (65536, (2048, 2048), Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } + + #[test] + fn test_mova_nova() -> Result<(), Box> { + let mut rng = test_rng(); + + test_ivc::, Pedersen, GriffinSponge<_>>, _>( + (65536, 2048, Arc::new(GriffinParams::new(16, 5, 9))), + CircuitForTest { + x: Fr::rand(&mut rng), + }, + vec![(); 20], + &mut rng, + )?; + + Ok(()) + } +} From 2c858b258b286ab0a35dc88691cd4ae21f57fab8 Mon Sep 17 00:00:00 2001 From: winderica Date: Tue, 25 Nov 2025 00:52:37 +0800 Subject: [PATCH 91/93] Split impls into separate submodules --- .../fs/src/mova/algorithms/key_generator.rs | 25 ++ crates/fs/src/mova/algorithms/mod.rs | 4 + crates/fs/src/mova/algorithms/preprocessor.rs | 13 + crates/fs/src/mova/algorithms/prover.rs | 135 +++++++++ crates/fs/src/mova/algorithms/verifier.rs | 57 ++++ crates/fs/src/mova/circuits/mod.rs | 1 + crates/fs/src/mova/circuits/verifier.rs | 62 ++++ .../mova/{instance => instances}/circuits.rs | 2 +- .../src/mova/{instance => instances}/mod.rs | 0 crates/fs/src/mova/mod.rs | 279 +----------------- .../mova/{witness => witnesses}/circuits.rs | 2 +- .../fs/src/mova/{witness => witnesses}/mod.rs | 4 +- 12 files changed, 313 insertions(+), 271 deletions(-) create mode 100644 crates/fs/src/mova/algorithms/key_generator.rs create mode 100644 crates/fs/src/mova/algorithms/mod.rs create mode 100644 crates/fs/src/mova/algorithms/preprocessor.rs create mode 100644 crates/fs/src/mova/algorithms/prover.rs create mode 100644 crates/fs/src/mova/algorithms/verifier.rs create mode 100644 crates/fs/src/mova/circuits/mod.rs create mode 100644 crates/fs/src/mova/circuits/verifier.rs rename crates/fs/src/mova/{instance => instances}/circuits.rs (100%) rename crates/fs/src/mova/{instance => instances}/mod.rs (100%) rename crates/fs/src/mova/{witness => witnesses}/circuits.rs (100%) rename crates/fs/src/mova/{witness => witnesses}/mod.rs (87%) diff --git a/crates/fs/src/mova/algorithms/key_generator.rs b/crates/fs/src/mova/algorithms/key_generator.rs new file mode 100644 index 000000000..dace8d80f --- /dev/null +++ b/crates/fs/src/mova/algorithms/key_generator.rs @@ -0,0 +1,25 @@ +use ark_std::sync::Arc; +use sonobe_primitives::{ + arithmetizations::{Arith, ArithConfig}, + commitments::{CommitmentKey, GroupBasedCommitment}, +}; + +use crate::{ + Error, FoldingSchemeKeyGenerator, + mova::{Mova, MovaKey}, +}; + +impl FoldingSchemeKeyGenerator + for Mova +{ + fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { + let ck = Arc::new(ck); + let r1cs = Arc::new(r1cs); + if ck.max_scalars_len() < r1cs.config().n_witnesses() { + return Err(Error::InvalidPublicParameters( + "The commitment key is too short for the R1CS instance".into(), + )); + } + Ok(MovaKey { arith: r1cs, ck }) + } +} diff --git a/crates/fs/src/mova/algorithms/mod.rs b/crates/fs/src/mova/algorithms/mod.rs new file mode 100644 index 000000000..11e838298 --- /dev/null +++ b/crates/fs/src/mova/algorithms/mod.rs @@ -0,0 +1,4 @@ +pub mod key_generator; +pub mod preprocessor; +pub mod prover; +pub mod verifier; diff --git a/crates/fs/src/mova/algorithms/preprocessor.rs b/crates/fs/src/mova/algorithms/preprocessor.rs new file mode 100644 index 000000000..c2bbaf27f --- /dev/null +++ b/crates/fs/src/mova/algorithms/preprocessor.rs @@ -0,0 +1,13 @@ +use ark_std::rand::RngCore; +use sonobe_primitives::commitments::GroupBasedCommitment; + +use crate::{Error, FoldingSchemePreprocessor, mova::Mova}; + +impl FoldingSchemePreprocessor + for Mova +{ + fn preprocess(n_witnesses: usize, mut rng: impl RngCore) -> Result { + let ck = CM::generate_key(n_witnesses, &mut rng)?; + Ok(ck) + } +} diff --git a/crates/fs/src/mova/algorithms/prover.rs b/crates/fs/src/mova/algorithms/prover.rs new file mode 100644 index 000000000..68814228c --- /dev/null +++ b/crates/fs/src/mova/algorithms/prover.rs @@ -0,0 +1,135 @@ +use ark_ff::{One, Zero}; +use ark_poly::{ + DenseMultilinearExtension as MLE, DenseUVPolynomial, Polynomial, univariate::DensePolynomial, +}; +use ark_std::{borrow::Borrow, cfg_into_iter, cfg_iter, rand::RngCore}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::{bits::FromBits, poly::MLEHelper}, + arithmetizations::{Arith, ArithConfig}, + circuits::AssignmentsOwned, + commitments::GroupBasedCommitment, + transcripts::Transcript, +}; + +use crate::{ + Error, FoldingSchemeProver, + mova::{Mova, MovaKey, MovaProof}, +}; + +impl FoldingSchemeProver<1, 1> + for Mova +{ + #[allow(non_snake_case)] + fn prove( + pk: &MovaKey, + transcript: &mut impl Transcript, + Ws: &[impl Borrow; 1], + Us: &[impl Borrow; 1], + ws: &[impl Borrow; 1], + us: &[impl Borrow; 1], + rng: impl RngCore, + ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { + let (W, U) = (Ws[0].borrow(), Us[0].borrow()); + let (w, u) = (ws[0].borrow(), us[0].borrow()); + + // Protocol 5 + + // Step 5.1: Commit to w & send commitment + let (cm_w, r_w) = CM::commit(&pk.ck, w, rng)?; + + transcript.add(U); + transcript.add(u); + transcript.add(&cm_w); + + // Step 5.2: Get challenge r_E + let r_e = transcript.challenge_field_elements(U.r_e.len()); + + // Protocol 6 + + // Step 6.1: Compute l(X) such that l(0) = r1, l(1) = r2 + let l = U + .r_e + .iter() + .zip(&r_e) + .map(|(&r1, &r2)| DensePolynomial::from_coefficients_vec(vec![r1, r2 - r1])) + .collect::>(); + // Step 6.1: Compute h1(X) and h2(X), where h2(X) is empty in our case + let h1 = { + // Initialize the polynomial vector from the evaluations in the MLE. + // Each evaluation is turned into a constant polynomial. + let mut poly = + W.e.iter() + .chain(vec![Zero::zero(); 1 << U.r_e.len()].iter()) + .map(|&x| DensePolynomial::from_coefficients_slice(&[x])) + .collect::>(); + + for i in &l { + poly = poly + .chunks_exact(2) + .map(|w| &w[0] + (&w[1] - &w[0]).naive_mul(i)) + .collect(); + } + + poly.swap_remove(0) + }; + // Step 6.1: Send h1(X) and h2(X), where the constant term is omitted + // because it always equals v + let mut h1_coeffs = h1.coeffs.clone(); + h1_coeffs.resize(pk.arith.config().log_constraints() + 1, Zero::zero()); + h1_coeffs.remove(0); + transcript.add(&h1_coeffs); + + // Step 6.2: Get challenge beta + let beta = transcript.challenge_field_element(); + + // Step 6.3: Compute r_E' + let r_e_prime = l.iter().map(|i| i.evaluate(&beta)).collect(); + + // Protocol 7 + + // Step 7.1: Compute cross term `T`. We follow the optimized approach in + // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. + let v = pk.arith.evaluate_at(AssignmentsOwned::from(( + U.u + CM::Scalar::one(), + cfg_iter!(U.x).zip(&u[..]).map(|(a, b)| *a + b).collect(), + cfg_iter!(W.w).zip(&w[..]).map(|(a, b)| *a + b).collect(), + )))?; + let T = cfg_into_iter!(v) + .zip(&W.e) + .map(|(a, b)| a - b) + .collect::>(); + // Step 7.1: Evaluate & send T's MLE at r_E' + let t = MLE::from_evaluations(&T).evaluate(&r_e_prime); + transcript.add(&t); + + // Step 7.2: Get challenge rho + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + // Step 7.3: Compute new W and U + Ok(( + Self::RW { + e: cfg_iter!(W.e).zip(&T).map(|(a, b)| rho * b + a).collect(), + w: cfg_iter!(W.w) + .zip(&w[..]) + .map(|(a, b)| rho * b + a) + .collect(), + r_w: W.r_w + r_w * rho, + }, + Self::RU { + r_e: r_e_prime, + v: h1.evaluate(&beta) + rho * t, + u: U.u + rho, + cm_w: U.cm_w + cm_w * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }, + MovaProof { h1_coeffs, t, cm_w }, + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/mova/algorithms/verifier.rs b/crates/fs/src/mova/algorithms/verifier.rs new file mode 100644 index 000000000..dc7655472 --- /dev/null +++ b/crates/fs/src/mova/algorithms/verifier.rs @@ -0,0 +1,57 @@ +use ark_poly::{DenseUVPolynomial, Polynomial, univariate::DensePolynomial}; +use ark_std::{borrow::Borrow, cfg_iter}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; +use sonobe_primitives::{ + algebra::ops::bits::FromBits, commitments::GroupBasedCommitment, transcripts::Transcript, +}; + +use crate::{Error, FoldingSchemeVerifier, mova::Mova}; + +impl FoldingSchemeVerifier<1, 1> + for Mova +{ + #[allow(non_snake_case)] + fn verify( + _vk: &(), + transcript: &mut impl Transcript, + Us: &[impl Borrow; 1], + us: &[impl Borrow; 1], + proof: &Self::Proof<1, 1>, + ) -> Result { + let (U, u) = (Us[0].borrow(), us[0].borrow()); + + let h1 = DensePolynomial::from_coefficients_vec([&[U.v][..], &proof.h1_coeffs].concat()); + + transcript.add(U); + transcript.add(u); + transcript.add(&proof.cm_w); + + let r_e = transcript.challenge_field_elements(U.r_e.len()); + + transcript.add(&proof.h1_coeffs); + + let beta = transcript.challenge_field_element(); + + transcript.add(&proof.t); + + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); + let rho = CM::Scalar::from_bits_le(&rho_bits); + + Ok(Self::RU { + r_e: U + .r_e + .iter() + .zip(r_e) + .map(|(&r1, r2)| r1 + beta * (r2 - r1)) + .collect(), + v: h1.evaluate(&beta) + rho * proof.t, + u: U.u + rho, + cm_w: U.cm_w + proof.cm_w * rho, + x: cfg_iter!(U.x) + .zip(&u[..]) + .map(|(a, b)| rho * b + a) + .collect(), + }) + } +} diff --git a/crates/fs/src/mova/circuits/mod.rs b/crates/fs/src/mova/circuits/mod.rs new file mode 100644 index 000000000..9a0722027 --- /dev/null +++ b/crates/fs/src/mova/circuits/mod.rs @@ -0,0 +1 @@ +pub mod verifier; diff --git a/crates/fs/src/mova/circuits/verifier.rs b/crates/fs/src/mova/circuits/verifier.rs new file mode 100644 index 000000000..9a362b3fd --- /dev/null +++ b/crates/fs/src/mova/circuits/verifier.rs @@ -0,0 +1,62 @@ +use ark_r1cs_std::{ + GR1CSVar, alloc::AllocVar, fields::fp::FpVar, + poly::polynomial::univariate::dense::DensePolynomialVar, +}; +use ark_relations::gr1cs::SynthesisError; +use sonobe_primitives::{ + algebra::ops::bits::FromBitsGadget, commitments::GroupBasedCommitment, + transcripts::TranscriptGadget, +}; + +use crate::{FoldingSchemePartialVerifierGadget, mova::MovaGadget}; + +impl FoldingSchemePartialVerifierGadget<1, 1> + for MovaGadget +{ + #[allow(non_snake_case)] + fn verify_hinted( + _vk: &Self::VerifierKey, + transcript: &mut impl TranscriptGadget, + [U]: [&Self::RU; 1], + [u]: [&Self::IU; 1], + proof: &Self::Proof<1, 1>, + ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { + let h1 = DensePolynomialVar::from_coefficients_vec( + [&[U.v.clone()][..], &proof.h1_coeffs].concat(), + ); + + transcript.add(U)?; + transcript.add(u)?; + transcript.add(&proof.cm_w)?; + + let r_e = transcript.challenge_field_elements(U.r_e.len())?; + + transcript.add(&proof.h1_coeffs)?; + + let beta = transcript.challenge_field_element()?; + + transcript.add(&proof.t)?; + + let rho_bits = transcript.challenge_bits(CHALLENGE_BITS)?; + let rho = FpVar::from_bits_le(&rho_bits)?; + + Ok(( + Self::RU { + r_e: U + .r_e + .iter() + .zip(r_e) + .map(|(r1, r2)| r1 + &beta * (r2 - r1)) + .collect(), + v: h1.evaluate(&beta)? + &rho * &proof.t, + u: &U.u + &rho, + cm_w: AllocVar::new_witness(U.cm_w.cs().or(proof.cm_w.cs()).or(rho.cs()), || { + Ok(U.cm_w.value().unwrap_or_default() + + proof.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) + })?, + x: U.x.iter().zip(&u[..]).map(|(a, b)| &rho * b + a).collect(), + }, + rho_bits.try_into().unwrap(), + )) + } +} diff --git a/crates/fs/src/mova/instance/circuits.rs b/crates/fs/src/mova/instances/circuits.rs similarity index 100% rename from crates/fs/src/mova/instance/circuits.rs rename to crates/fs/src/mova/instances/circuits.rs index 4d6addc82..7373afd18 100644 --- a/crates/fs/src/mova/instance/circuits.rs +++ b/crates/fs/src/mova/instances/circuits.rs @@ -1,9 +1,9 @@ use ark_r1cs_std::{ + GR1CSVar, alloc::{AllocVar, AllocationMode}, boolean::Boolean, fields::fp::FpVar, select::CondSelectGadget, - GR1CSVar, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::borrow::Borrow; diff --git a/crates/fs/src/mova/instance/mod.rs b/crates/fs/src/mova/instances/mod.rs similarity index 100% rename from crates/fs/src/mova/instance/mod.rs rename to crates/fs/src/mova/instances/mod.rs diff --git a/crates/fs/src/mova/mod.rs b/crates/fs/src/mova/mod.rs index 144f1ba15..f33dfbed3 100644 --- a/crates/fs/src/mova/mod.rs +++ b/crates/fs/src/mova/mod.rs @@ -1,52 +1,38 @@ -use ark_ff::{Field, One, Zero}; -use ark_poly::{ - DenseMultilinearExtension as MLE, DenseUVPolynomial, Polynomial, univariate::DensePolynomial, -}; +use ark_ff::{Field, Zero}; +use ark_poly::{DenseMultilinearExtension as MLE, Polynomial}; use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, boolean::Boolean, fields::fp::FpVar, - poly::polynomial::univariate::dense::DensePolynomialVar, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{ - UniformRand, borrow::Borrow, cfg_into_iter, cfg_iter, marker::PhantomData, rand::RngCore, - sync::Arc, -}; -#[cfg(feature = "parallel")] -use rayon::prelude::*; +use ark_std::{UniformRand, borrow::Borrow, marker::PhantomData, rand::RngCore, sync::Arc}; use sonobe_primitives::{ - algebra::ops::{ - bits::{FromBits, FromBitsGadget}, - poly::MLEHelper, - }, + algebra::ops::poly::MLEHelper, arithmetizations::{ Arith, ArithConfig, ArithRelation, r1cs::{R1CS, RelaxedInstance, RelaxedWitness}, }, circuits::AssignmentsOwned, - commitments::{ - CommitmentDef, CommitmentDefGadget, CommitmentKey, CommitmentOps, GroupBasedCommitment, - }, + commitments::{CommitmentDef, CommitmentDefGadget, CommitmentOps, GroupBasedCommitment}, relations::{Relation, WitnessInstanceSampler}, traits::{CF1, Dummy, SonobeCurve}, - transcripts::{Transcript, TranscriptGadget}, }; use self::{ - instance::{RunningInstance as RU, circuits::RunningInstanceVar as RUVar}, - witness::RunningWitness as RW, + instances::{RunningInstance as RU, circuits::RunningInstanceVar as RUVar}, + witnesses::RunningWitness as RW, }; use crate::{ - DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, FoldingSchemeKeyGenerator, - FoldingSchemePartialVerifierGadget, FoldingSchemePreprocessor, FoldingSchemeProver, - FoldingSchemeVerifier, GroupBasedFoldingSchemePrimaryDef, PlainInstance as IU, - PlainInstanceVar as IUVar, PlainWitness as IW, + DeciderKey, Error, FoldingSchemeDef, FoldingSchemeDefGadget, GroupBasedFoldingSchemePrimaryDef, + PlainInstance as IU, PlainInstanceVar as IUVar, PlainWitness as IW, }; -pub mod instance; -pub mod witness; +pub mod algorithms; +pub mod circuits; +pub mod instances; +pub mod witnesses; #[derive(Clone)] pub struct MovaKey { @@ -204,194 +190,6 @@ impl FoldingSchemeDef type Proof = MovaProof; } -impl FoldingSchemePreprocessor - for Mova -{ - fn preprocess(n_witnesses: usize, mut rng: impl RngCore) -> Result { - let ck = CM::generate_key(n_witnesses, &mut rng)?; - Ok(ck) - } -} - -impl FoldingSchemeKeyGenerator - for Mova -{ - fn generate_keys(ck: Self::PublicParam, r1cs: Self::Arith) -> Result { - let ck = Arc::new(ck); - let r1cs = Arc::new(r1cs); - if ck.max_scalars_len() < r1cs.config().n_witnesses() { - return Err(Error::InvalidPublicParameters( - "The commitment key is too short for the R1CS instance".into(), - )); - } - Ok(MovaKey { arith: r1cs, ck }) - } -} - -impl FoldingSchemeProver<1, 1> - for Mova -{ - #[allow(non_snake_case)] - fn prove( - pk: &MovaKey, - transcript: &mut impl Transcript, - Ws: &[impl Borrow; 1], - Us: &[impl Borrow; 1], - ws: &[impl Borrow; 1], - us: &[impl Borrow; 1], - rng: impl RngCore, - ) -> Result<(Self::RW, Self::RU, Self::Proof<1, 1>, Self::Challenge), Error> { - let (W, U) = (Ws[0].borrow(), Us[0].borrow()); - let (w, u) = (ws[0].borrow(), us[0].borrow()); - - // Protocol 5 - - // Step 5.1: Commit to w & send commitment - let (cm_w, r_w) = CM::commit(&pk.ck, w, rng)?; - - transcript.add(U); - transcript.add(u); - transcript.add(&cm_w); - - // Step 5.2: Get challenge r_E - let r_e = transcript.challenge_field_elements(U.r_e.len()); - - // Protocol 6 - - // Step 6.1: Compute l(X) such that l(0) = r1, l(1) = r2 - let l = U - .r_e - .iter() - .zip(&r_e) - .map(|(&r1, &r2)| DensePolynomial::from_coefficients_vec(vec![r1, r2 - r1])) - .collect::>(); - // Step 6.1: Compute h1(X) and h2(X), where h2(X) is empty in our case - let h1 = { - // Initialize the polynomial vector from the evaluations in the MLE. - // Each evaluation is turned into a constant polynomial. - let mut poly = - W.e.iter() - .chain(vec![Zero::zero(); 1 << U.r_e.len()].iter()) - .map(|&x| DensePolynomial::from_coefficients_slice(&[x])) - .collect::>(); - - for i in &l { - poly = poly - .chunks_exact(2) - .map(|w| &w[0] + (&w[1] - &w[0]).naive_mul(i)) - .collect(); - } - - poly.swap_remove(0) - }; - // Step 6.1: Send h1(X) and h2(X), where the constant term is omitted - // because it always equals v - let mut h1_coeffs = h1.coeffs.clone(); - h1_coeffs.resize(pk.arith.config().log_constraints() + 1, Zero::zero()); - h1_coeffs.remove(0); - transcript.add(&h1_coeffs); - - // Step 6.2: Get challenge beta - let beta = transcript.challenge_field_element(); - - // Step 6.3: Compute r_E' - let r_e_prime = l.iter().map(|i| i.evaluate(&beta)).collect(); - - // Protocol 7 - - // Step 7.1: Compute cross term `T`. We follow the optimized approach in - // [Mova](https://eprint.iacr.org/2024/1220.pdf)'s section 5.2. - let v = pk.arith.evaluate_at(AssignmentsOwned::from(( - U.u + CM::Scalar::one(), - cfg_iter!(U.x).zip(&u[..]).map(|(a, b)| *a + b).collect(), - cfg_iter!(W.w).zip(&w[..]).map(|(a, b)| *a + b).collect(), - )))?; - let T = cfg_into_iter!(v) - .zip(&W.e) - .map(|(a, b)| a - b) - .collect::>(); - // Step 7.1: Evaluate & send T's MLE at r_E' - let t = MLE::from_evaluations(&T).evaluate(&r_e_prime); - transcript.add(&t); - - // Step 7.2: Get challenge rho - let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); - let rho = CM::Scalar::from_bits_le(&rho_bits); - - // Step 7.3: Compute new W and U - Ok(( - Self::RW { - e: cfg_iter!(W.e).zip(&T).map(|(a, b)| rho * b + a).collect(), - w: cfg_iter!(W.w) - .zip(&w[..]) - .map(|(a, b)| rho * b + a) - .collect(), - r_w: W.r_w + r_w * rho, - }, - Self::RU { - r_e: r_e_prime, - v: h1.evaluate(&beta) + rho * t, - u: U.u + rho, - cm_w: U.cm_w + cm_w * rho, - x: cfg_iter!(U.x) - .zip(&u[..]) - .map(|(a, b)| rho * b + a) - .collect(), - }, - MovaProof { h1_coeffs, t, cm_w }, - rho_bits.try_into().unwrap(), - )) - } -} - -impl FoldingSchemeVerifier<1, 1> - for Mova -{ - #[allow(non_snake_case)] - fn verify( - _vk: &(), - transcript: &mut impl Transcript, - Us: &[impl Borrow; 1], - us: &[impl Borrow; 1], - proof: &Self::Proof<1, 1>, - ) -> Result { - let (U, u) = (Us[0].borrow(), us[0].borrow()); - - let h1 = DensePolynomial::from_coefficients_vec([&[U.v][..], &proof.h1_coeffs].concat()); - - transcript.add(U); - transcript.add(u); - transcript.add(&proof.cm_w); - - let r_e = transcript.challenge_field_elements(U.r_e.len()); - - transcript.add(&proof.h1_coeffs); - - let beta = transcript.challenge_field_element(); - - transcript.add(&proof.t); - - let rho_bits = transcript.challenge_bits(CHALLENGE_BITS); - let rho = CM::Scalar::from_bits_le(&rho_bits); - - Ok(Self::RU { - r_e: U - .r_e - .iter() - .zip(r_e) - .map(|(&r1, r2)| r1 + beta * (r2 - r1)) - .collect(), - v: h1.evaluate(&beta) + rho * proof.t, - u: U.u + rho, - cm_w: U.cm_w + proof.cm_w * rho, - x: cfg_iter!(U.x) - .zip(&u[..]) - .map(|(a, b)| rho * b + a) - .collect(), - }) - } -} - #[derive(Clone)] pub struct MovaProofVar { pub h1_coeffs: Vec>>, @@ -451,57 +249,6 @@ impl FoldingSchemeDefGadg type Proof = MovaProofVar; } -impl FoldingSchemePartialVerifierGadget<1, 1> - for MovaGadget -{ - #[allow(non_snake_case)] - fn verify_hinted( - _vk: &Self::VerifierKey, - transcript: &mut impl TranscriptGadget, - [U]: [&Self::RU; 1], - [u]: [&Self::IU; 1], - proof: &Self::Proof<1, 1>, - ) -> Result<(Self::RU, Self::Challenge), SynthesisError> { - let h1 = DensePolynomialVar::from_coefficients_vec( - [&[U.v.clone()][..], &proof.h1_coeffs].concat(), - ); - - transcript.add(U)?; - transcript.add(u)?; - transcript.add(&proof.cm_w)?; - - let r_e = transcript.challenge_field_elements(U.r_e.len())?; - - transcript.add(&proof.h1_coeffs)?; - - let beta = transcript.challenge_field_element()?; - - transcript.add(&proof.t)?; - - let rho_bits = transcript.challenge_bits(CHALLENGE_BITS)?; - let rho = FpVar::from_bits_le(&rho_bits)?; - - Ok(( - RUVar { - r_e: U - .r_e - .iter() - .zip(r_e) - .map(|(r1, r2)| r1 + &beta * (r2 - r1)) - .collect(), - v: h1.evaluate(&beta)? + &rho * &proof.t, - u: &U.u + &rho, - cm_w: AllocVar::new_witness(U.cm_w.cs().or(proof.cm_w.cs()).or(rho.cs()), || { - Ok(U.cm_w.value().unwrap_or_default() - + proof.cm_w.value().unwrap_or_default() * rho.value().unwrap_or_default()) - })?, - x: U.x.iter().zip(&u[..]).map(|(a, b)| &rho * b + a).collect(), - }, - rho_bits.try_into().unwrap(), - )) - } -} - impl GroupBasedFoldingSchemePrimaryDef for Mova { diff --git a/crates/fs/src/mova/witness/circuits.rs b/crates/fs/src/mova/witnesses/circuits.rs similarity index 100% rename from crates/fs/src/mova/witness/circuits.rs rename to crates/fs/src/mova/witnesses/circuits.rs index d0212f153..54cf8c7a9 100644 --- a/crates/fs/src/mova/witness/circuits.rs +++ b/crates/fs/src/mova/witnesses/circuits.rs @@ -1,6 +1,6 @@ use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, GR1CSVar, + alloc::{AllocVar, AllocationMode}, }; use ark_relations::gr1cs::{ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::borrow::Borrow; diff --git a/crates/fs/src/mova/witness/mod.rs b/crates/fs/src/mova/witnesses/mod.rs similarity index 87% rename from crates/fs/src/mova/witness/mod.rs rename to crates/fs/src/mova/witnesses/mod.rs index 4a54b7b45..59c095b11 100644 --- a/crates/fs/src/mova/witness/mod.rs +++ b/crates/fs/src/mova/witnesses/mod.rs @@ -1,6 +1,4 @@ -use sonobe_primitives::{ - arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, -}; +use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; use crate::FoldingWitness; From 7c71e0fc288ea58df091a090b74d7b2ca8e94ec7 Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 6 Feb 2026 18:35:53 +0800 Subject: [PATCH 92/93] Actually test wasm targets --- crates/fs/src/mova/mod.rs | 9 +++++++-- crates/ivc/src/compilers/cyclefold/adapters/mova.rs | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/fs/src/mova/mod.rs b/crates/fs/src/mova/mod.rs index f33dfbed3..46303bf4c 100644 --- a/crates/fs/src/mova/mod.rs +++ b/crates/fs/src/mova/mod.rs @@ -259,11 +259,16 @@ impl GroupBasedFoldingSch mod tests { use ark_bn254::{Fr, G1Projective}; use ark_ff::UniformRand; - use ark_std::{error::Error, rand::Rng, test_rng}; + use ark_std::{ + error::Error, + rand::{Rng, thread_rng}, + }; use sonobe_primitives::{ circuits::utils::{CircuitForTest, satisfying_assignments_for_test}, commitments::pedersen::Pedersen, }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::tests::test_folding_scheme; @@ -295,7 +300,7 @@ mod tests { #[test] fn test_mova() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); test_mova_opt(10, &mut rng)?; Ok(()) } diff --git a/crates/ivc/src/compilers/cyclefold/adapters/mova.rs b/crates/ivc/src/compilers/cyclefold/adapters/mova.rs index d358867e2..0a1d3a818 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/mova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/mova.rs @@ -108,19 +108,21 @@ mod tests { use ark_bn254::{Fr, G1Projective as C1}; use ark_ff::UniformRand; use ark_grumpkin::Projective as C2; - use ark_std::{error::Error, sync::Arc, test_rng}; + use ark_std::{error::Error, rand::thread_rng, sync::Arc}; use sonobe_primitives::{ circuits::utils::CircuitForTest, commitments::pedersen::Pedersen, transcripts::griffin::{GriffinParams, sponge::GriffinSponge}, }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + use wasm_bindgen_test::wasm_bindgen_test as test; use super::*; use crate::tests::test_ivc; #[test] fn test_mova_ova() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); test_ivc::, Pedersen, GriffinSponge<_>>, _>( (65536, (2048, 2048), Arc::new(GriffinParams::new(16, 5, 9))), @@ -136,7 +138,7 @@ mod tests { #[test] fn test_mova_nova() -> Result<(), Box> { - let mut rng = test_rng(); + let mut rng = thread_rng(); test_ivc::, Pedersen, GriffinSponge<_>>, _>( (65536, 2048, Arc::new(GriffinParams::new(16, 5, 9))), From 707b234559d3933d8c39747ecca55e83c0399e2c Mon Sep 17 00:00:00 2001 From: winderica Date: Fri, 13 Feb 2026 14:10:33 +0800 Subject: [PATCH 93/93] Add Mova docs --- .../fs/src/mova/algorithms/key_generator.rs | 2 ++ crates/fs/src/mova/algorithms/mod.rs | 2 ++ crates/fs/src/mova/algorithms/preprocessor.rs | 2 ++ crates/fs/src/mova/algorithms/prover.rs | 2 ++ crates/fs/src/mova/algorithms/verifier.rs | 2 ++ crates/fs/src/mova/circuits/mod.rs | 2 ++ crates/fs/src/mova/circuits/verifier.rs | 2 ++ crates/fs/src/mova/instances/circuits.rs | 13 +++++++++-- crates/fs/src/mova/instances/mod.rs | 13 +++++++++-- crates/fs/src/mova/mod.rs | 22 +++++++++++++++++-- crates/fs/src/mova/witnesses/circuits.rs | 6 +++++ crates/fs/src/mova/witnesses/mod.rs | 7 ++++++ .../src/compilers/cyclefold/adapters/mova.rs | 8 ++++++- 13 files changed, 76 insertions(+), 7 deletions(-) diff --git a/crates/fs/src/mova/algorithms/key_generator.rs b/crates/fs/src/mova/algorithms/key_generator.rs index dace8d80f..29c3f2d22 100644 --- a/crates/fs/src/mova/algorithms/key_generator.rs +++ b/crates/fs/src/mova/algorithms/key_generator.rs @@ -1,3 +1,5 @@ +//! Key generation for Mova. + use ark_std::sync::Arc; use sonobe_primitives::{ arithmetizations::{Arith, ArithConfig}, diff --git a/crates/fs/src/mova/algorithms/mod.rs b/crates/fs/src/mova/algorithms/mod.rs index 11e838298..d7d329ec9 100644 --- a/crates/fs/src/mova/algorithms/mod.rs +++ b/crates/fs/src/mova/algorithms/mod.rs @@ -1,3 +1,5 @@ +//! Implementations folding scheme algorithms for Mova. + pub mod key_generator; pub mod preprocessor; pub mod prover; diff --git a/crates/fs/src/mova/algorithms/preprocessor.rs b/crates/fs/src/mova/algorithms/preprocessor.rs index c2bbaf27f..fc8c56e72 100644 --- a/crates/fs/src/mova/algorithms/preprocessor.rs +++ b/crates/fs/src/mova/algorithms/preprocessor.rs @@ -1,3 +1,5 @@ +//! Preprocessing for Mova. + use ark_std::rand::RngCore; use sonobe_primitives::commitments::GroupBasedCommitment; diff --git a/crates/fs/src/mova/algorithms/prover.rs b/crates/fs/src/mova/algorithms/prover.rs index 68814228c..be6632db9 100644 --- a/crates/fs/src/mova/algorithms/prover.rs +++ b/crates/fs/src/mova/algorithms/prover.rs @@ -1,3 +1,5 @@ +//! Proof generation for Mova. + use ark_ff::{One, Zero}; use ark_poly::{ DenseMultilinearExtension as MLE, DenseUVPolynomial, Polynomial, univariate::DensePolynomial, diff --git a/crates/fs/src/mova/algorithms/verifier.rs b/crates/fs/src/mova/algorithms/verifier.rs index dc7655472..131014bb6 100644 --- a/crates/fs/src/mova/algorithms/verifier.rs +++ b/crates/fs/src/mova/algorithms/verifier.rs @@ -1,3 +1,5 @@ +//! Proof verification for Mova. + use ark_poly::{DenseUVPolynomial, Polynomial, univariate::DensePolynomial}; use ark_std::{borrow::Borrow, cfg_iter}; #[cfg(feature = "parallel")] diff --git a/crates/fs/src/mova/circuits/mod.rs b/crates/fs/src/mova/circuits/mod.rs index 9a0722027..9ed16d86a 100644 --- a/crates/fs/src/mova/circuits/mod.rs +++ b/crates/fs/src/mova/circuits/mod.rs @@ -1 +1,3 @@ +//! In-circuit gadgets for Mova. + pub mod verifier; diff --git a/crates/fs/src/mova/circuits/verifier.rs b/crates/fs/src/mova/circuits/verifier.rs index 9a362b3fd..777b6090e 100644 --- a/crates/fs/src/mova/circuits/verifier.rs +++ b/crates/fs/src/mova/circuits/verifier.rs @@ -1,3 +1,5 @@ +//! Partial in-circuit verifier implementation for Mova. + use ark_r1cs_std::{ GR1CSVar, alloc::AllocVar, fields::fp::FpVar, poly::polynomial::univariate::dense::DensePolynomialVar, diff --git a/crates/fs/src/mova/instances/circuits.rs b/crates/fs/src/mova/instances/circuits.rs index 7373afd18..2c3ff0220 100644 --- a/crates/fs/src/mova/instances/circuits.rs +++ b/crates/fs/src/mova/instances/circuits.rs @@ -1,3 +1,5 @@ +//! In-circuit variables for Mova instances. + use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, @@ -12,14 +14,21 @@ use sonobe_primitives::{commitments::CommitmentDefGadget, transcripts::Absorbabl use super::RunningInstance; use crate::FoldingInstanceVar; +/// [`RunningInstanceVar`] defines Mova's running instance variable. #[derive(Clone, Debug, PartialEq)] pub struct RunningInstanceVar { - // Random evaluation point for the E + /// [`RunningInstanceVar::r_e`] is the random evaluation point for the error + /// term. pub r_e: Vec, - // Evaluation of the MLE of E at r_E + /// [`RunningInstanceVar::v`] is the evaluation of the MLE of the error term + /// at `r_e`. pub v: CM::ScalarVar, + /// [`RunningInstanceVar::u`] is the constant term. pub u: CM::ScalarVar, + /// [`RunningInstanceVar::cm_w`] is the witness commitment. pub cm_w: CM::CommitmentVar, + /// [`RunningInstanceVar::x`] is the vector of public inputs (to the + /// circuit). pub x: Vec, } diff --git a/crates/fs/src/mova/instances/mod.rs b/crates/fs/src/mova/instances/mod.rs index a084d62c3..c09db8283 100644 --- a/crates/fs/src/mova/instances/mod.rs +++ b/crates/fs/src/mova/instances/mod.rs @@ -1,3 +1,6 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Mova +//! instances. + use ark_ff::PrimeField; use sonobe_primitives::{ arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy, @@ -8,14 +11,20 @@ use crate::FoldingInstance; pub mod circuits; +/// [`RunningInstance`] defines Mova's running instance. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningInstance { - // Random evaluation point for the E + /// [`RunningInstance::r_e`] is the random evaluation point for the error + /// term. pub r_e: Vec, - // Evaluation of the MLE of E at r_E + /// [`RunningInstance::v`] is the evaluation of the MLE of the error term + /// at `r_e`. pub v: CM::Scalar, + /// [`RunningInstance::u`] is the constant term. pub u: CM::Scalar, + /// [`RunningInstance::cm_w`] is the witness commitment. pub cm_w: CM::Commitment, + /// [`RunningInstance::x`] is the vector of public inputs (to the circuit). pub x: Vec, } diff --git a/crates/fs/src/mova/mod.rs b/crates/fs/src/mova/mod.rs index 46303bf4c..db1965c7e 100644 --- a/crates/fs/src/mova/mod.rs +++ b/crates/fs/src/mova/mod.rs @@ -1,3 +1,8 @@ +//! This module implements the Mova folding scheme, which is introduced in this +//! [paper]. +//! +//! [paper]: https://eprint.iacr.org/2024/1220.pdf + use ark_ff::{Field, Zero}; use ark_poly::{DenseMultilinearExtension as MLE, Polynomial}; use ark_r1cs_std::{ @@ -34,10 +39,11 @@ pub mod circuits; pub mod instances; pub mod witnesses; +/// [`MovaKey`] is Mova's decider key. #[derive(Clone)] pub struct MovaKey { - pub arith: Arc, - pub ck: Arc, + arith: Arc, + ck: Arc, } impl DeciderKey for MovaKey { @@ -150,10 +156,15 @@ where } } +/// [`MovaProof`] is Mova's proof. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MovaProof { + /// [`MovaProof::h1_coeffs`] is the `h_1` polynomial. pub h1_coeffs: Vec>, + /// [`MovaProof::t`] is the evaluation of the `T` polynomial's MLE at the + /// challenge point `r_e`. pub t: CF1, + /// [`MovaProof::cm_w`] is the witness commitment. pub cm_w: C, } @@ -167,6 +178,7 @@ impl Dummy<&Cfg> for MovaProof { } } +/// [`Mova`] implements the Mova folding scheme. pub struct Mova { _t: PhantomData, } @@ -190,10 +202,15 @@ impl FoldingSchemeDef type Proof = MovaProof; } +/// [`MovaProofVar`] is the in-circuit variable for [`MovaProof`]. #[derive(Clone)] pub struct MovaProofVar { + /// [`MovaProofVar::h1_coeffs`] is the `h_1` polynomial. pub h1_coeffs: Vec>>, + /// [`MovaProofVar::t`] is the evaluation of the `T` polynomial's MLE at the + /// challenge point `r_e`. pub t: FpVar>, + /// [`MovaProofVar::cm_w`] is the witness commitment. pub cm_w: C::EmulatedVar>, } @@ -232,6 +249,7 @@ impl GR1CSVar> for MovaProofVar { } } +/// [`MovaGadget`] is the in-circuit gadget for [`Mova`]. pub struct MovaGadget { _t: PhantomData, } diff --git a/crates/fs/src/mova/witnesses/circuits.rs b/crates/fs/src/mova/witnesses/circuits.rs index 54cf8c7a9..55462be6a 100644 --- a/crates/fs/src/mova/witnesses/circuits.rs +++ b/crates/fs/src/mova/witnesses/circuits.rs @@ -1,3 +1,5 @@ +//! In-circuit variables for Mova witnesses. + use ark_r1cs_std::{ GR1CSVar, alloc::{AllocVar, AllocationMode}, @@ -8,10 +10,14 @@ use sonobe_primitives::commitments::CommitmentDefGadget; use super::RunningWitness; +/// [`RunningWitnessVar`] defines Mova's running witness variable. #[derive(Debug, PartialEq)] pub struct RunningWitnessVar { + /// [`RunningWitnessVar::w`] is the witness (to the circuit). pub w: Vec, + /// [`RunningWitnessVar::r_w`] is the randomness for the witness commitment. pub r_w: CM::RandomnessVar, + /// [`RunningWitnessVar::e`] is the error term. pub e: Vec, } diff --git a/crates/fs/src/mova/witnesses/mod.rs b/crates/fs/src/mova/witnesses/mod.rs index 59c095b11..348ec0427 100644 --- a/crates/fs/src/mova/witnesses/mod.rs +++ b/crates/fs/src/mova/witnesses/mod.rs @@ -1,13 +1,20 @@ +//! Definitions of out-of-circuit values and in-circuit variables for Mova +//! witnesses. + use sonobe_primitives::{arithmetizations::ArithConfig, commitments::CommitmentDef, traits::Dummy}; use crate::FoldingWitness; pub mod circuits; +/// [`RunningWitness`] defines Mova's running witness. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RunningWitness { + /// [`RunningWitness::w`] is the witness (to the circuit). pub w: Vec, + /// [`RunningWitness::r_w`] is the randomness for the witness commitment. pub r_w: CM::Randomness, + /// [`RunningWitness::e`] is the error term. pub e: Vec, } diff --git a/crates/ivc/src/compilers/cyclefold/adapters/mova.rs b/crates/ivc/src/compilers/cyclefold/adapters/mova.rs index 0a1d3a818..7bafb509e 100644 --- a/crates/ivc/src/compilers/cyclefold/adapters/mova.rs +++ b/crates/ivc/src/compilers/cyclefold/adapters/mova.rs @@ -1,3 +1,5 @@ +//! Mova CycleFold adapter that bridges Mova into the CycleFold IVC compiler. + use ark_ff::{PrimeField, Zero}; use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, prelude::Boolean}; use ark_relations::gr1cs::{ConstraintSystemRef, SynthesisError}; @@ -16,7 +18,7 @@ use crate::compilers::cyclefold::{ CycleFoldBasedIVC, FoldingSchemeCycleFoldExt, circuits::CycleFoldCircuit, }; -/// Configuration for Mova's CycleFold circuit +/// [`MovaCycleFoldCircuit`] defines CycleFold circuit for Mova. pub struct MovaCycleFoldCircuit { r: Vec, points: Vec, @@ -97,9 +99,13 @@ impl FoldingSchemeCycleFo } } +/// [`MovaOvaIVC`] defines a CycleFold-based IVC using Mova as the primary +/// folding scheme and Ova as the secondary folding scheme. pub type MovaOvaIVC = CycleFoldBasedIVC, CycleFoldOva, T>; +/// [`MovaNovaIVC`] defines a CycleFold-based IVC using Mova as the primary +/// folding scheme and Nova as the secondary folding scheme. pub type MovaNovaIVC = CycleFoldBasedIVC, CycleFoldNova, T>;