From e198b436e8c508a2df18c31bd432738bb8739a10 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 10 Dec 2025 17:20:56 -0800 Subject: [PATCH 01/24] export APIs and add doc --- src/constants.rs | 17 +++-- src/digest.rs | 9 +++ src/gadgets/ecc.rs | 32 ++++++---- src/gadgets/mod.rs | 12 +++- src/gadgets/nonnative/bignat.rs | 18 ++++++ src/gadgets/nonnative/mod.rs | 3 + src/gadgets/nonnative/util.rs | 15 +++-- src/lib.rs | 8 +-- src/r1cs/mod.rs | 103 ++++++++++++++++++++++++++++++- src/spartan/math.rs | 7 +++ src/spartan/mod.rs | 72 ++++++++++++++++----- src/spartan/polys/eq.rs | 3 +- src/spartan/polys/mod.rs | 14 ++++- src/spartan/polys/multilinear.rs | 14 +++-- src/spartan/polys/power.rs | 15 +++-- src/spartan/sumcheck.rs | 38 ++++++++++-- 16 files changed, 313 insertions(+), 67 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index c458a5ddd..3a286a2b5 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,13 @@ -pub(crate) const NUM_CHALLENGE_BITS: usize = 128; -pub(crate) const NUM_HASH_BITS: usize = 250; -pub(crate) const BN_LIMB_WIDTH: usize = 64; -pub(crate) const BN_N_LIMBS: usize = 4; +//! This module defines constants used throughout the library. + +/// Number of bits used for challenge generation in the protocol. +pub const NUM_CHALLENGE_BITS: usize = 128; + +/// Number of bits used for hash output sizing. +pub const NUM_HASH_BITS: usize = 250; + +/// Width of each limb in bignat representation. +pub const BN_LIMB_WIDTH: usize = 64; + +/// Number of limbs in bignat representation. +pub const BN_N_LIMBS: usize = 4; diff --git a/src/digest.rs b/src/digest.rs index e063449a3..b4d2c24cb 100644 --- a/src/digest.rs +++ b/src/digest.rs @@ -1,3 +1,10 @@ +//! This module provides digest computation functionality for public parameters. +//! +//! It includes: +//! - `DigestComputer`: A type for computing digests of public parameters. +//! - `Digestible`: A trait for types that can be digested with custom implementations. +//! - `SimpleDigestible`: A marker trait for types that can be digested via serialization. + use crate::constants::NUM_HASH_BITS; use ff::PrimeField; use serde::Serialize; @@ -26,6 +33,8 @@ impl Digestible for T { } } +/// A type for computing digests of public parameters. +/// Uses SHA3-256 and maps the result to a field element. pub struct DigestComputer<'a, F: PrimeField, T> { inner: &'a T, _phantom: PhantomData, diff --git a/src/gadgets/ecc.rs b/src/gadgets/ecc.rs index a262a9c86..e3fd05da6 100644 --- a/src/gadgets/ecc.rs +++ b/src/gadgets/ecc.rs @@ -21,9 +21,12 @@ use num_bigint::BigInt; /// `AllocatedPoint` provides an elliptic curve abstraction inside a circuit. #[derive(Clone)] pub struct AllocatedPoint { - pub(crate) x: AllocatedNum, - pub(crate) y: AllocatedNum, - pub(crate) is_infinity: AllocatedNum, + /// The x-coordinate of the point. + pub x: AllocatedNum, + /// The y-coordinate of the point. + pub y: AllocatedNum, + /// Flag indicating if this is the point at infinity (1 = infinity, 0 = not infinity). + pub is_infinity: AllocatedNum, } impl AllocatedPoint @@ -584,8 +587,10 @@ where #[derive(Clone)] /// `AllocatedPoint` but one that is guaranteed to be not infinity pub struct AllocatedPointNonInfinity { - x: AllocatedNum, - y: AllocatedNum, + /// The x-coordinate of the point. + pub x: AllocatedNum, + /// The y-coordinate of the point. + pub y: AllocatedNum, } impl AllocatedPointNonInfinity { @@ -742,18 +747,21 @@ impl AllocatedPointNonInfinity { } } -// `AllocatedNonnativePoint`s are points on an elliptic curve E'. We use the scalar field -// of another curve E (specified as the group G) to prove things about points on E'. -// `AllocatedNonnativePoint`s are always represented as affine coordinates. +/// `AllocatedNonnativePoint`s are points on an elliptic curve E'. We use the scalar field +/// of another curve E (specified as the group G) to prove things about points on E'. +/// `AllocatedNonnativePoint`s are always represented as affine coordinates. #[derive(Clone, Debug)] pub struct AllocatedNonnativePoint { - pub(crate) x: BigNat, - pub(crate) y: BigNat, - pub(crate) is_infinity: AllocatedNum, + /// The x-coordinate as a BigNat. + pub x: BigNat, + /// The y-coordinate as a BigNat. + pub y: BigNat, + /// Flag indicating if this is the point at infinity. + pub is_infinity: AllocatedNum, } -#[allow(dead_code)] impl AllocatedNonnativePoint { + /// Allocates a new nonnative point from coordinates. pub fn alloc>( mut cs: CS, coords: Option<(E::Base, E::Base, bool)>, diff --git a/src/gadgets/mod.rs b/src/gadgets/mod.rs index 483cf1151..4f1dd989d 100644 --- a/src/gadgets/mod.rs +++ b/src/gadgets/mod.rs @@ -1,4 +1,10 @@ //! This module implements various gadgets necessary for Nova and applications built with Nova. -pub(crate) mod ecc; -pub(crate) mod nonnative; -pub(crate) mod utils; + +/// Elliptic curve gadgets for in-circuit point operations. +pub mod ecc; + +/// Non-native field arithmetic gadgets for operations on fields with different characteristics. +pub mod nonnative; + +/// Utility gadgets for common operations like conditional selection, bit manipulation, etc. +pub mod utils; diff --git a/src/gadgets/nonnative/bignat.rs b/src/gadgets/nonnative/bignat.rs index 0f9b67dfd..e649cf05d 100644 --- a/src/gadgets/nonnative/bignat.rs +++ b/src/gadgets/nonnative/bignat.rs @@ -57,15 +57,21 @@ pub fn nat_to_limbs( } } +/// Parameters for a `BigNat` representation of a large integer. #[derive(Clone, Debug, PartialEq, Eq)] pub struct BigNatParams { + /// Minimum number of bits required to represent the value. pub min_bits: usize, + /// Maximum value that a single limb can hold. pub max_word: BigInt, + /// Number of bits per limb. pub limb_width: usize, + /// Number of limbs in the representation. pub n_limbs: usize, } impl BigNatParams { + /// Creates a new `BigNatParams` with the specified limb width and number of limbs. pub fn new(limb_width: usize, n_limbs: usize) -> Self { let mut max_word = BigInt::from(1) << limb_width as u32; max_word -= 1; @@ -243,6 +249,7 @@ impl BigNat { Ok(bignat) } + /// Returns the limbs as a vector of `Num` values. pub fn as_limbs(&self) -> Vec> { let mut limbs = Vec::new(); for (i, lc) in self.limbs.iter().enumerate() { @@ -254,6 +261,7 @@ impl BigNat { limbs } + /// Asserts that the `BigNat` is well-formed by checking that each limb fits in the limb width. pub fn assert_well_formed>( &self, mut cs: CS, @@ -309,6 +317,8 @@ impl BigNat { }) } + /// Enforces that two `BigNat`s have the same limb width. + /// Returns the limb width if they agree, otherwise returns an error. pub fn enforce_limb_width_agreement( &self, other: &Self, @@ -325,6 +335,7 @@ impl BigNat { } } + /// Constructs a `BigNat` from a polynomial representation. pub fn from_poly(poly: Polynomial, limb_width: usize, max_word: BigInt) -> Self { Self { params: BigNatParams { @@ -443,6 +454,7 @@ impl BigNat { self_grouped.equal_when_carried(cs.namespace(|| "grouped"), &other_grouped) } + /// Adds two `BigNat`s together, returning a new `BigNat` with the sum. pub fn add(&self, other: &Self) -> Result, SynthesisError> { self.enforce_limb_width_agreement(other, "add")?; let n_limbs = max(self.params.n_limbs, other.params.n_limbs); @@ -658,18 +670,23 @@ impl BigNat { } } + /// Returns the number of bits required to represent the `BigNat`. pub fn n_bits(&self) -> usize { assert!(self.params.n_limbs > 0); self.params.limb_width * (self.params.n_limbs - 1) + self.params.max_word.bits() as usize } } +/// A polynomial with coefficients represented as linear combinations. pub struct Polynomial { + /// The coefficients of the polynomial as linear combinations. pub coefficients: Vec>, + /// The concrete values of the coefficients, if available. pub values: Option>, } impl Polynomial { + /// Allocates constraints for the product of two polynomials. pub fn alloc_product>( &self, mut cs: CS, @@ -735,6 +752,7 @@ impl Polynomial { Ok(product) } + /// Returns the sum of two polynomials. pub fn sum(&self, other: &Self) -> Self { let n_coeffs = max(self.coefficients.len(), other.coefficients.len()); let values = self.values.as_ref().and_then(|self_vs| { diff --git a/src/gadgets/nonnative/mod.rs b/src/gadgets/nonnative/mod.rs index d2d590dfc..e68f93d28 100644 --- a/src/gadgets/nonnative/mod.rs +++ b/src/gadgets/nonnative/mod.rs @@ -31,5 +31,8 @@ impl BitAccess for Scalar { } } +/// Module providing big natural number arithmetic in circuits. pub mod bignat; + +/// Module providing utility types and functions for non-native arithmetic. pub mod util; diff --git a/src/gadgets/nonnative/util.rs b/src/gadgets/nonnative/util.rs index e02435665..7f16e1de2 100644 --- a/src/gadgets/nonnative/util.rs +++ b/src/gadgets/nonnative/util.rs @@ -13,14 +13,14 @@ use std::{ #[derive(Clone)] /// A representation of a bit pub struct Bit { - /// The linear combination which constrain the value of the bit + /// The linear combination which constrains the value of the bit pub bit: LinearCombination, } #[derive(Clone)] /// A representation of a bit-vector pub struct Bitvector { - /// The linear combination which constrain the values of the bits + /// The linear combination which constrains the values of the bits pub bits: Vec>, /// The value of the bits (filled at witness-time) pub values: Option>, @@ -61,15 +61,20 @@ impl Bit { } } +/// A representation of a field element as a linear combination with an optional value. pub struct Num { - pub(crate) num: LinearCombination, - pub(crate) value: Option, + /// The linear combination representing the number. + pub num: LinearCombination, + /// The value of the number (filled at witness-time). + pub value: Option, } impl Num { + /// Creates a new `Num` with the given value and linear combination. pub const fn new(value: Option, num: LinearCombination) -> Self { Self { value, num } } + /// Allocates a new `Num` in the constraint system with the given value. pub fn alloc(mut cs: CS, value: F) -> Result where CS: ConstraintSystem, @@ -93,6 +98,7 @@ impl Num { }) } + /// Checks that the `Num` fits in the given number of bits. pub fn fits_in_bits>( &self, mut cs: CS, @@ -211,6 +217,7 @@ impl Num { }) } + /// Converts the `Num` to an `AllocatedNum` in the constraint system. pub fn as_allocated_num>( &self, mut cs: CS, diff --git a/src/lib.rs b/src/lib.rs index 43c836647..365894006 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,11 +23,9 @@ pub mod gadgets; pub mod provider; pub mod spartan; pub mod traits; - -// private modules -mod constants; -mod digest; -mod r1cs; +pub mod constants; +pub mod digest; +pub mod r1cs; use traits::{commitment::CommitmentEngineTrait, Engine}; diff --git a/src/r1cs/mod.rs b/src/r1cs/mod.rs index 7abb4790d..1a2d28096 100644 --- a/src/r1cs/mod.rs +++ b/src/r1cs/mod.rs @@ -21,7 +21,7 @@ use rayon::prelude::*; use serde::{Deserialize, Serialize}; mod sparse; -pub(crate) use sparse::SparseMatrix; +pub use sparse::SparseMatrix; /// A type that holds the shape of the R1CS matrices #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -72,9 +72,46 @@ pub struct RelaxedR1CSInstance { pub(crate) u: E::Scalar, } +/// A type alias for a function that provides hints about the commitment key size needed for an R1CS shape. pub type CommitmentKeyHint = dyn Fn(&R1CSShape) -> usize; impl R1CSShape { + /// Returns the number of constraints in the R1CS shape. + /// + /// This is useful for computing the number of rounds in sumcheck. + pub fn num_cons(&self) -> usize { + self.num_cons + } + + /// Returns the number of variables in the R1CS shape. + /// + /// This is useful for computing the number of rounds in sumcheck. + pub fn num_vars(&self) -> usize { + self.num_vars + } + + /// Returns the number of public inputs/outputs in the R1CS shape. + /// + /// This is useful for dimension validation. + pub fn num_io(&self) -> usize { + self.num_io + } + + /// Returns a reference to the A matrix of the R1CS shape. + pub fn A(&self) -> &SparseMatrix { + &self.A + } + + /// Returns a reference to the B matrix of the R1CS shape. + pub fn B(&self) -> &SparseMatrix { + &self.B + } + + /// Returns a reference to the C matrix of the R1CS shape. + pub fn C(&self) -> &SparseMatrix { + &self.C + } + /// Create an object of type `R1CSShape` from the explicitly specified R1CS matrices pub fn new( num_cons: usize, @@ -152,6 +189,7 @@ impl R1CSShape { cons_valid && vars_valid && io_lt_vars } + /// Multiplies the R1CS matrices A, B, C by a vector z and returns (Az, Bz, Cz). pub fn multiply_vec( &self, z: &[E::Scalar], @@ -452,6 +490,13 @@ impl R1CSWitness { }) } + /// Returns a reference to the witness vector W. + /// + /// This is useful for cloning witness values for matrix commitments. + pub fn W(&self) -> &[E::Scalar] { + &self.W + } + /// Commits to the witness using the supplied generators pub fn commit(&self, ck: &CommitmentKey) -> Commitment { CE::::commit(ck, &self.W, &self.r_W) @@ -482,6 +527,18 @@ impl R1CSInstance { }) } } + + /// Returns a reference to the commitment to the witness. + pub fn comm_W(&self) -> &Commitment { + &self.comm_W + } + + /// Returns a reference to the public inputs/outputs. + /// + /// This is useful for public IO indexing (e.g., `inst.X()[i]`). + pub fn X(&self) -> &[E::Scalar] { + &self.X + } } impl AbsorbInROTrait for R1CSInstance { @@ -528,6 +585,20 @@ impl RelaxedR1CSWitness { } } + /// Returns a reference to the witness vector W. + /// + /// This is useful for witness manipulation. + pub fn W(&self) -> &[E::Scalar] { + &self.W + } + + /// Returns a reference to the error vector E. + /// + /// This is useful for error term manipulation. + pub fn E(&self) -> &[E::Scalar] { + &self.E + } + /// Commits to the witness using the supplied generators pub fn commit(&self, ck: &CommitmentKey) -> (Commitment, Commitment) { ( @@ -618,6 +689,7 @@ impl RelaxedR1CSWitness { } } + /// Derandomizes the witness by setting randomness to zero. pub fn derandomize(&self) -> (Self, E::Scalar, E::Scalar) { ( RelaxedR1CSWitness { @@ -658,6 +730,34 @@ impl RelaxedR1CSInstance { r_instance } + /// Returns a reference to the commitment to the witness W. + /// + /// This is useful for commitment operations. + pub fn comm_W(&self) -> &Commitment { + &self.comm_W + } + + /// Returns a reference to the commitment to the error vector E. + /// + /// This is useful for commitment operations. + pub fn comm_E(&self) -> &Commitment { + &self.comm_E + } + + /// Returns a reference to the public inputs/outputs. + /// + /// This is useful for public IO access. + pub fn X(&self) -> &[E::Scalar] { + &self.X + } + + /// Returns the relaxation factor u. + /// + /// This is useful for accessing the relaxation factor in folding. + pub fn u(&self) -> E::Scalar { + self.u + } + /// Initializes a new `RelaxedR1CSInstance` from an `R1CSInstance` pub fn from_r1cs_instance_unchecked( comm_W: &Commitment, @@ -729,6 +829,7 @@ impl RelaxedR1CSInstance { } } + /// Derandomizes the instance by removing the randomness from the commitments. pub fn derandomize( &self, dk: &DerandKey, diff --git a/src/spartan/math.rs b/src/spartan/math.rs index 22dbce175..5c8f63bab 100644 --- a/src/spartan/math.rs +++ b/src/spartan/math.rs @@ -1,4 +1,11 @@ +//! This module provides mathematical utilities for Spartan. + +/// A trait providing mathematical operations on `usize`. pub trait Math { + /// Computes the base-2 logarithm of the value. + /// + /// # Panics + /// Panics if the value is zero. fn log_2(self) -> usize; } diff --git a/src/spartan/mod.rs b/src/spartan/mod.rs index 332f243b1..a75a92ae8 100644 --- a/src/spartan/mod.rs +++ b/src/spartan/mod.rs @@ -11,9 +11,15 @@ pub mod snark; #[macro_use] mod macros; -pub(crate) mod math; -pub(crate) mod polys; -pub(crate) mod sumcheck; + +/// Module providing the `Math` trait with `log_2()` method on `usize`. +pub mod math; + +/// Module providing polynomial types for Spartan SNARKs. +pub mod polys; + +/// Module providing sumcheck protocol implementation. +pub mod sumcheck; pub use sumcheck::SumcheckEngine; @@ -26,8 +32,10 @@ use ff::Field; use itertools::Itertools as _; use rayon::{iter::IntoParallelRefIterator, prelude::*}; -// Creates a vector of the first `n` powers of `s`. -fn powers(s: &E::Scalar, n: usize) -> Vec { +/// Creates a vector of the first `n` powers of `s`. +/// +/// Returns `[1, s, s^2, ..., s^{n-1}]`. +pub fn powers(s: &E::Scalar, n: usize) -> Vec { assert!(n >= 1); let mut powers = Vec::with_capacity(n); powers.push(E::Scalar::ONE); @@ -38,11 +46,17 @@ fn powers(s: &E::Scalar, n: usize) -> Vec { } /// A type that holds a witness to a polynomial evaluation instance -struct PolyEvalWitness { +#[derive(Clone, Debug)] +pub struct PolyEvalWitness { p: Vec, // polynomial } impl PolyEvalWitness { + /// Returns a reference to the polynomial coefficients. + pub fn p(&self) -> &[E::Scalar] { + &self.p + } + /// Given [Pᵢ] and s, compute P = ∑ᵢ sⁱ⋅Pᵢ /// /// # Details @@ -165,13 +179,29 @@ impl PolyEvalWitness { } /// A type that holds a polynomial evaluation instance -struct PolyEvalInstance { +#[derive(Clone, Debug)] +pub struct PolyEvalInstance { c: Commitment, // commitment to the polynomial x: Vec, // evaluation point e: E::Scalar, // claimed evaluation } impl PolyEvalInstance { + /// Returns a reference to the commitment to the polynomial. + pub fn c(&self) -> &Commitment { + &self.c + } + + /// Returns a reference to the evaluation point. + pub fn x(&self) -> &[E::Scalar] { + &self.x + } + + /// Returns the claimed evaluation. + pub fn e(&self) -> E::Scalar { + self.e + } + fn batch_diff_size( c_vec: &[Commitment], e_vec: &[E::Scalar], @@ -240,12 +270,22 @@ impl PolyEvalInstance { } } -/// Bounds "row" variables of (A, B, C) matrices viewed as 2d multilinear polynomials -fn compute_eval_table_sparse( +/// Bounds "row" variables of (A, B, C) matrices viewed as 2d multilinear polynomials. +/// +/// Given an R1CS shape and evaluation point `rx`, computes the evaluations of the +/// A, B, and C matrices when their row variables are bound to `rx`. +/// +/// # Arguments +/// * `S` - The R1CS shape containing the A, B, C matrices +/// * `rx` - The evaluation point for row variables (length must equal num_cons) +/// +/// # Returns +/// A tuple of three vectors (A_evals, B_evals, C_evals), each of length 2 * num_vars. +pub fn compute_eval_table_sparse( S: &R1CSShape, rx: &[E::Scalar], ) -> (Vec, Vec, Vec) { - assert_eq!(rx.len(), S.num_cons); + assert_eq!(rx.len(), S.num_cons()); let inner = |M: &SparseMatrix, M_evals: &mut Vec| { for (row_idx, ptrs) in M.indptr.windows(2).enumerate() { @@ -257,20 +297,20 @@ fn compute_eval_table_sparse( let (A_evals, (B_evals, C_evals)) = rayon::join( || { - let mut A_evals: Vec = vec![E::Scalar::ZERO; 2 * S.num_vars]; - inner(&S.A, &mut A_evals); + let mut A_evals: Vec = vec![E::Scalar::ZERO; 2 * S.num_vars()]; + inner(S.A(), &mut A_evals); A_evals }, || { rayon::join( || { - let mut B_evals: Vec = vec![E::Scalar::ZERO; 2 * S.num_vars]; - inner(&S.B, &mut B_evals); + let mut B_evals: Vec = vec![E::Scalar::ZERO; 2 * S.num_vars()]; + inner(S.B(), &mut B_evals); B_evals }, || { - let mut C_evals: Vec = vec![E::Scalar::ZERO; 2 * S.num_vars]; - inner(&S.C, &mut C_evals); + let mut C_evals: Vec = vec![E::Scalar::ZERO; 2 * S.num_vars()]; + inner(S.C(), &mut C_evals); C_evals }, ) diff --git a/src/spartan/polys/eq.rs b/src/spartan/polys/eq.rs index 84f76deb9..51ac2c863 100644 --- a/src/spartan/polys/eq.rs +++ b/src/spartan/polys/eq.rs @@ -15,7 +15,8 @@ use rayon::prelude::*; /// For instance, for e = 6 (with a binary representation of 0b110), the vector r would be [1, 1, 0]. #[derive(Debug)] pub struct EqPolynomial { - pub(in crate::spartan::polys) r: Vec, + /// The vector of scalars representing the equality polynomial. + pub r: Vec, } impl EqPolynomial { diff --git a/src/spartan/polys/mod.rs b/src/spartan/polys/mod.rs index a1a192ef8..08f8a5d03 100644 --- a/src/spartan/polys/mod.rs +++ b/src/spartan/polys/mod.rs @@ -1,7 +1,15 @@ //! This module contains the definitions of polynomial types used in the Spartan SNARK. -pub(crate) mod eq; + +/// Module providing the equality polynomial. +pub mod eq; + pub(crate) mod identity; pub(crate) mod masked_eq; -pub(crate) mod multilinear; -pub(crate) mod power; + +/// Module providing multilinear polynomial types. +pub mod multilinear; + +/// Module providing power polynomial. +pub mod power; + pub(crate) mod univariate; diff --git a/src/spartan/polys/multilinear.rs b/src/spartan/polys/multilinear.rs index 9f08f337c..37e007f28 100644 --- a/src/spartan/polys/multilinear.rs +++ b/src/spartan/polys/multilinear.rs @@ -27,8 +27,9 @@ use serde::{Deserialize, Serialize}; /// Vector $Z$ indicates $Z(e)$ where $e$ ranges from $0$ to $2^m-1$. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MultilinearPolynomial { - num_vars: usize, // the number of variables in the multilinear polynomial - pub(crate) Z: Vec, // evaluations of the polynomial in all the 2^num_vars Boolean inputs + num_vars: usize, // the number of variables in the multilinear polynomial + /// The evaluations of the polynomial in all the 2^num_vars Boolean inputs + pub Z: Vec, } impl MultilinearPolynomial { @@ -111,17 +112,19 @@ impl Index for MultilinearPolynomial { /// Sparse multilinear polynomial, which means the $Z(\cdot)$ is zero at most points. /// In our context, sparse polynomials are non-zeros over the hypercube at locations that map to "small" integers /// We exploit this property to implement a time-optimal algorithm -pub(crate) struct SparsePolynomial { +pub struct SparsePolynomial { num_vars: usize, - Z: Vec, + /// The non-zero evaluations + pub Z: Vec, } impl SparsePolynomial { + /// Creates a new `SparsePolynomial` from the given number of variables and evaluations. pub fn new(num_vars: usize, Z: Vec) -> Self { SparsePolynomial { num_vars, Z } } - // a time-optimal algorithm to evaluate sparse polynomials + /// A time-optimal algorithm to evaluate sparse polynomials pub fn evaluate(&self, r: &[Scalar]) -> Scalar { assert_eq!(self.num_vars, r.len()); @@ -141,7 +144,6 @@ impl SparsePolynomial { common * eval_partial } } - /// Adds another multilinear polynomial to `self`. /// Assumes the two polynomials have the same number of variables. impl Add for MultilinearPolynomial { diff --git a/src/spartan/polys/power.rs b/src/spartan/polys/power.rs index 270653133..d33b1988a 100644 --- a/src/spartan/polys/power.rs +++ b/src/spartan/polys/power.rs @@ -3,20 +3,20 @@ use core::iter::successors; use ff::PrimeField; -/// Represents the multilinear extension polynomial (MLE) of the equality polynomial $pow(x,t)$, denoted as $\tilde{pow}(x, t)$. +/// Represents the multilinear extension polynomial (MLE) of the power polynomial $pow(x,t)$, denoted as $\tilde{pow}(x, t)$. /// /// The polynomial is defined by the formula: /// $$ /// \tilde{power}(x, t) = \prod_{i=1}^m(1 + (t^{2^i} - 1) * x_i) /// $$ -#[allow(dead_code)] pub struct PowPolynomial { t_pow: Vec, } -#[allow(dead_code)] impl PowPolynomial { - /// Creates a new `PowPolynomial` from a Scalars `t`. + /// Creates a new `PowPolynomial` from a scalar `t` and number of variables `ell`. + /// + /// The internal representation stores `[t^{2^0}, t^{2^1}, ..., t^{2^{ell-1}}]`. pub fn new(t: &Scalar, ell: usize) -> Self { // t_pow = [t^{2^0}, t^{2^1}, ..., t^{2^{ell-1}}] let t_pow = successors(Some(*t), |p: &Scalar| Some(p.square())) @@ -29,15 +29,18 @@ impl PowPolynomial { /// Evaluates the `PowPolynomial` at all the `2^|t_pow|` points in its domain. /// /// Returns a vector of Scalars, each corresponding to the polynomial evaluation at a specific point. - #[cfg(test)] pub fn evals(&self) -> Vec { successors(Some(Scalar::ONE), |p| Some(*p * self.t_pow[0])) .take(1 << self.t_pow.len()) .collect::>() } + /// Returns the coordinates (powers of t) used in this polynomial. + pub fn coordinates(&self) -> Vec { + self.t_pow.clone() + } + /// Computes two vectors such that their outer product equals the output of the `evals` function. - /// This code ensures pub fn split_evals(&self, len_left: usize, len_right: usize) -> Vec { // Compute the number of elements in the left and right halves let ell = self.t_pow.len(); diff --git a/src/spartan/sumcheck.rs b/src/spartan/sumcheck.rs index 8433923e0..935880bf8 100644 --- a/src/spartan/sumcheck.rs +++ b/src/spartan/sumcheck.rs @@ -35,17 +35,29 @@ pub trait SumcheckEngine: Send + Sync { fn final_claims(&self) -> Vec>; } +/// A proof generated by the sumcheck protocol. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "")] -pub(crate) struct SumcheckProof { +pub struct SumcheckProof { compressed_polys: Vec>, } impl SumcheckProof { + /// Creates a new `SumcheckProof` from compressed univariate polynomials. pub fn new(compressed_polys: Vec>) -> Self { Self { compressed_polys } } + /// Verifies the sumcheck proof. + /// + /// # Arguments + /// * `claim` - The claimed sum + /// * `num_rounds` - The number of sumcheck rounds + /// * `degree_bound` - The degree bound of the univariate polynomials + /// * `transcript` - The transcript for Fiat-Shamir + /// + /// # Returns + /// A tuple of (final_evaluation, challenges) if verification succeeds. pub fn verify( &self, claim: E::Scalar, @@ -88,6 +100,8 @@ impl SumcheckProof { Ok((e, r)) } + /// Verifies a batch of sumcheck instances with different numbers of rounds. + /// Claims are combined using random linear combination with the provided coefficients. pub fn verify_batch( &self, claims: &[E::Scalar], @@ -147,7 +161,17 @@ impl SumcheckProof { ) } - // comb_func: |p, q| p * q + /// Proves a sumcheck for `sum_x poly_A(x) * poly_B(x)`. + /// + /// # Arguments + /// * `claim` - The claimed sum + /// * `num_rounds` - The number of sumcheck rounds + /// * `poly_A` - First multilinear polynomial (will be mutated) + /// * `poly_B` - Second multilinear polynomial (will be mutated) + /// * `transcript` - The transcript for Fiat-Shamir + /// + /// # Returns + /// A tuple of (proof, challenges, final_evaluations). pub fn prove_quad_prod( claim: &E::Scalar, num_rounds: usize, @@ -193,7 +217,8 @@ impl SumcheckProof { )) } - // comb_func: |p, q| p * q + /// Proves a batch of quadratic sumcheck instances over product polynomials. + /// Each instance computes the sum over the boolean hypercube of poly_A[i] * poly_B[i]. pub fn prove_quad_batch_prod( claims: &[E::Scalar], num_rounds: &[usize], @@ -316,9 +341,10 @@ impl SumcheckProof { Ok((SumcheckProof::new(quad_polys), r, claims_prod)) } - // DEG1: poly_A - poly_B - // DEG2: poly_A * poly_B - // DEG3: poly_A * poly_B * poly_C + /// Computes evaluation points for a cubic sumcheck round. + /// DEG1: poly_A - poly_B + /// DEG2: poly_A * poly_B + /// DEG3: poly_A * poly_B * poly_C #[inline] pub fn compute_eval_points_cubic_with_deg( poly_A: &MultilinearPolynomial, From 4832179cb96a32ad08120cbc118dab132b5c8fc3 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 10 Dec 2025 17:21:10 -0800 Subject: [PATCH 02/24] cargo fmt --- src/lib.rs | 6 +++--- src/spartan/polys/multilinear.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 365894006..4052e5bcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,15 +17,15 @@ pub mod nova; pub mod neutron; // public modules +pub mod constants; +pub mod digest; pub mod errors; pub mod frontend; pub mod gadgets; pub mod provider; +pub mod r1cs; pub mod spartan; pub mod traits; -pub mod constants; -pub mod digest; -pub mod r1cs; use traits::{commitment::CommitmentEngineTrait, Engine}; diff --git a/src/spartan/polys/multilinear.rs b/src/spartan/polys/multilinear.rs index 37e007f28..85e034705 100644 --- a/src/spartan/polys/multilinear.rs +++ b/src/spartan/polys/multilinear.rs @@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize}; /// Vector $Z$ indicates $Z(e)$ where $e$ ranges from $0$ to $2^m-1$. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MultilinearPolynomial { - num_vars: usize, // the number of variables in the multilinear polynomial + num_vars: usize, // the number of variables in the multilinear polynomial /// The evaluations of the polynomial in all the 2^num_vars Boolean inputs pub Z: Vec, } From b356499dbfe0efba9d2509b31dd0905e4f32232a Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 10 Dec 2025 17:25:17 -0800 Subject: [PATCH 03/24] address clippy --- src/spartan/polys/multilinear.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spartan/polys/multilinear.rs b/src/spartan/polys/multilinear.rs index 85e034705..22901ca4f 100644 --- a/src/spartan/polys/multilinear.rs +++ b/src/spartan/polys/multilinear.rs @@ -53,6 +53,11 @@ impl MultilinearPolynomial { self.Z.len() } + /// Returns true if the polynomial has no evaluations. + pub fn is_empty(&self) -> bool { + self.Z.is_empty() + } + /// Binds the polynomial's top variable using the given scalar. /// /// This operation modifies the polynomial in-place. From 32e70de4a5ed1d8c77c22a3e3226e59c2919c8bb Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 10 Dec 2025 17:28:22 -0800 Subject: [PATCH 04/24] Update src/spartan/sumcheck.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/spartan/sumcheck.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spartan/sumcheck.rs b/src/spartan/sumcheck.rs index 935880bf8..be2c201ea 100644 --- a/src/spartan/sumcheck.rs +++ b/src/spartan/sumcheck.rs @@ -343,7 +343,7 @@ impl SumcheckProof { /// Computes evaluation points for a cubic sumcheck round. /// DEG1: poly_A - poly_B - /// DEG2: poly_A * poly_B + /// DEG2: poly_A * poly_B /// DEG3: poly_A * poly_B * poly_C #[inline] pub fn compute_eval_points_cubic_with_deg( From 3e57d527f3eeb59529778c95a592c8bcc1fd098b Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 10 Dec 2025 17:28:36 -0800 Subject: [PATCH 05/24] Update src/spartan/polys/power.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/spartan/polys/power.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spartan/polys/power.rs b/src/spartan/polys/power.rs index d33b1988a..1e9639c31 100644 --- a/src/spartan/polys/power.rs +++ b/src/spartan/polys/power.rs @@ -36,8 +36,8 @@ impl PowPolynomial { } /// Returns the coordinates (powers of t) used in this polynomial. - pub fn coordinates(&self) -> Vec { - self.t_pow.clone() + pub fn coordinates(&self) -> &[Scalar] { + &self.t_pow } /// Computes two vectors such that their outer product equals the output of the `evals` function. From 9dd93689eafccaf5223221a3c2631eb676d0a724 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Thu, 11 Dec 2025 13:12:10 -0800 Subject: [PATCH 06/24] make nifs public --- src/nova/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nova/mod.rs b/src/nova/mod.rs index 6cb591d77..c23d4ebdf 100644 --- a/src/nova/mod.rs +++ b/src/nova/mod.rs @@ -30,7 +30,7 @@ use rand_core::OsRng; use serde::{Deserialize, Serialize}; mod circuit; -pub(crate) mod nifs; +pub mod nifs; use circuit::{NovaAugmentedCircuit, NovaAugmentedCircuitInputs}; use nifs::{NIFSRelaxed, NIFS}; From 69dc363b530d3d2b08f9a8a0ff0555d81383183a Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Tue, 16 Dec 2025 16:37:03 -0800 Subject: [PATCH 07/24] export a new method --- src/provider/pedersen.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index da6aad4e2..931808c93 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -199,6 +199,27 @@ where points.extend(self.ck.iter().cloned()); write_points(writer, points) } + + /// Returns the coordinates of the generator points. + /// + /// This method extracts the (x, y) coordinates of each generator point + /// in the commitment key. This is useful for operations that need direct + /// access to the underlying elliptic curve points. + /// + /// # Panics + /// + /// Panics if any generator point is the point at infinity. + pub fn to_coordinates(&self) -> Vec<(E::Base, E::Base)> { + self + .ck + .iter() + .map(|c| { + let (x, y, is_infinity) = ::group(c).to_coordinates(); + assert!(!is_infinity); + (x, y) + }) + .collect() + } } impl CommitmentEngineTrait for CommitmentEngine From fe2e64446430050ecfdcdce1b74f56691509106c Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Tue, 16 Dec 2025 17:01:38 -0800 Subject: [PATCH 08/24] Add ck_to_coordinates method to CommitmentEngineTrait This method allows external crates (like MicroNova) to extract the (x, y) coordinates of commitment key generator points. This is useful for in-circuit verification of polynomial evaluations. - Add ck_to_coordinates to CommitmentEngineTrait - Implement for PedersenCommitmentEngine (delegates to existing method) - Implement for HyperKZGCommitmentEngine (add to_coordinates method first) --- src/provider/hyperkzg.rs | 21 +++++++++++++++++++++ src/provider/pedersen.rs | 4 ++++ src/traits/commitment.rs | 12 ++++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index f9cb11a91..1f82d3024 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -82,6 +82,23 @@ where pub fn tau_H(&self) -> &<::G2 as DlogGroup>::AffineGroupElement { &self.tau_H } + + /// Returns the coordinates of the generator points. + /// + /// # Panics + /// + /// Panics if any generator point is the point at infinity. + pub fn to_coordinates(&self) -> Vec<(E::Base, E::Base)> { + self + .ck + .iter() + .map(|c| { + let (x, y, is_infinity) = ::group(c).to_coordinates(); + assert!(!is_infinity); + (x, y) + }) + .collect() + } } impl Len for CommitmentKey @@ -580,6 +597,10 @@ where Commitment { comm: res } } + + fn ck_to_coordinates(ck: &Self::CommitmentKey) -> Vec<(E::Base, E::Base)> { + ck.to_coordinates() + } } /// Provides an implementation of generators for proving evaluations diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index 931808c93..c7c96f5df 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -322,6 +322,10 @@ where h: first[0], }) } + + fn ck_to_coordinates(ck: &Self::CommitmentKey) -> Vec<(E::Base, E::Base)> { + ck.to_coordinates() + } } /// A trait listing properties of a commitment key that can be managed in a divide-and-conquer fashion diff --git a/src/traits/commitment.rs b/src/traits/commitment.rs index 003caf7ea..cdc7b7f10 100644 --- a/src/traits/commitment.rs +++ b/src/traits/commitment.rs @@ -124,4 +124,16 @@ pub trait CommitmentEngineTrait: Clone + Send + Sync { commit: &Self::Commitment, r: &E::Scalar, ) -> Self::Commitment; + + /// Returns the coordinates of each generator in the commitment key. + /// + /// This method extracts the (x, y) coordinates of each generator point + /// in the commitment key. This is useful for operations that need direct + /// access to the underlying elliptic curve points, such as in-circuit + /// verification of polynomial evaluations. + /// + /// # Panics + /// + /// Panics if any generator point is the point at infinity. + fn ck_to_coordinates(ck: &Self::CommitmentKey) -> Vec<(E::Base, E::Base)>; } From c332e29a625b57ad4924f025181efb6808b574d8 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 17 Dec 2025 11:42:26 -0800 Subject: [PATCH 09/24] export polys --- src/spartan/polys/mod.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/spartan/polys/mod.rs b/src/spartan/polys/mod.rs index 08f8a5d03..08772cfcc 100644 --- a/src/spartan/polys/mod.rs +++ b/src/spartan/polys/mod.rs @@ -3,8 +3,11 @@ /// Module providing the equality polynomial. pub mod eq; -pub(crate) mod identity; -pub(crate) mod masked_eq; +/// Module providing identity polynomial +pub mod identity; + +/// Module providing masked eq polynomial +pub mod masked_eq; /// Module providing multilinear polynomial types. pub mod multilinear; @@ -12,4 +15,5 @@ pub mod multilinear; /// Module providing power polynomial. pub mod power; -pub(crate) mod univariate; +/// Module providing univariate polynomial +pub mod univariate; From f893262dbf2c2b5ca357ed00f1346a4db3830392 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 17 Dec 2025 17:04:00 -0800 Subject: [PATCH 10/24] add doc --- src/provider/mercury.rs | 1 + src/spartan/polys/identity.rs | 4 ++++ src/spartan/polys/univariate.rs | 33 +++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/provider/mercury.rs b/src/provider/mercury.rs index a9d80dd1e..8b5cef310 100644 --- a/src/provider/mercury.rs +++ b/src/provider/mercury.rs @@ -120,6 +120,7 @@ impl UniPoly { self.coeffs.resize(size, Scalar::ZERO); } + /// Scales all coefficients of the polynomial by a scalar value. pub fn scale(&mut self, s: &Scalar) { self.coeffs.par_iter_mut().for_each(|c| *c *= *s); } diff --git a/src/spartan/polys/identity.rs b/src/spartan/polys/identity.rs index 72087551c..5fcb3ac1f 100644 --- a/src/spartan/polys/identity.rs +++ b/src/spartan/polys/identity.rs @@ -1,11 +1,14 @@ use core::marker::PhantomData; use ff::PrimeField; + +/// Represents an identity polynomial that maps a point to its index in binary representation. pub struct IdentityPolynomial { ell: usize, _p: PhantomData, } impl IdentityPolynomial { + /// Creates a new identity polynomial with the specified number of variables. pub fn new(ell: usize) -> Self { IdentityPolynomial { ell, @@ -13,6 +16,7 @@ impl IdentityPolynomial { } } + /// Evaluates the identity polynomial at the given point. pub fn evaluate(&self, r: &[Scalar]) -> Scalar { assert_eq!(self.ell, r.len()); let mut power_of_two = 1_u64; diff --git a/src/spartan/polys/univariate.rs b/src/spartan/polys/univariate.rs index ce9c12262..92c0d2ed9 100644 --- a/src/spartan/polys/univariate.rs +++ b/src/spartan/polys/univariate.rs @@ -7,15 +7,17 @@ use ff::PrimeField; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; -// ax^2 + bx + c stored as vec![c, b, a] -// ax^3 + bx^2 + cx + d stored as vec![d, c, b, a] +/// A univariate dense polynomial in coefficient form (little endian). +/// For example, ax^2 + bx + c is stored as vec![c, b, a] +/// and ax^3 + bx^2 + cx + d is stored as vec![d, c, b, a] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct UniPoly { pub(crate) coeffs: Vec, } -// ax^2 + bx + c stored as vec![c, a] -// ax^3 + bx^2 + cx + d stored as vec![d, c, a] +/// A compressed univariate polynomial with the linear term omitted (little endian). +/// For example, ax^2 + bx + c is stored as vec![c, a] +/// and ax^3 + bx^2 + cx + d is stored as vec![d, c, a] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CompressedUniPoly { coeffs_except_linear_term: Vec, @@ -44,8 +46,8 @@ impl UniPoly { Self { coeffs } } - // a x^2 + b x + c - // evals: [c, a + b + c, a] + /// Constructs a degree-2 polynomial from its evaluations. + /// The polynomial a*x^2 + b*x + c is constructed from evals: [c, a + b + c, a] pub fn from_evals_deg2(evals: &[Scalar]) -> Self { let c = evals[0]; let a = evals[2]; @@ -56,8 +58,8 @@ impl UniPoly { } } - // a x^3 + b x^2 + c x + d - // evals: [d, a + b + c, a, -a + b - c + d] + /// Constructs a degree-3 polynomial from its evaluations. + /// The polynomial a*x^3 + b*x^2 + c*x + d is constructed from evals: [d, a + b + c, a, -a + b - c + d] pub fn from_evals_deg3(evals: &[Scalar]) -> Self { let d = evals[0]; let a = evals[2]; @@ -70,14 +72,17 @@ impl UniPoly { } } + /// Returns the degree of the polynomial. pub fn degree(&self) -> usize { self.coeffs.len() - 1 } + /// Evaluates the polynomial at zero, returning the constant term. pub fn eval_at_zero(&self) -> Scalar { self.coeffs[0] } + /// Evaluates the polynomial at one, returning the sum of all coefficients. pub fn eval_at_one(&self) -> Scalar { (0..self.coeffs.len()) .into_par_iter() @@ -85,6 +90,7 @@ impl UniPoly { .sum() } + /// Evaluates the polynomial at the given point using Horner's method. pub fn evaluate(&self, r: &Scalar) -> Scalar { let mut eval = self.coeffs[0]; let mut power = *r; @@ -95,6 +101,7 @@ impl UniPoly { eval } + /// Compresses the polynomial by omitting the linear term. pub fn compress(&self) -> CompressedUniPoly { let coeffs_except_linear_term = [&self.coeffs[0..1], &self.coeffs[2..]].concat(); assert_eq!(coeffs_except_linear_term.len() + 1, self.coeffs.len()); @@ -105,8 +112,9 @@ impl UniPoly { } impl CompressedUniPoly { - // we require eval(0) + eval(1) = hint, so we can solve for the linear term as: - // linear_term = hint - 2 * constant_term - deg2 term - deg3 term + /// Decompresses the polynomial by recovering the linear term using the hint. + /// We require eval(0) + eval(1) = hint, so we can solve for the linear term as: + /// linear_term = hint - 2 * constant_term - deg2 term - deg3 term pub fn decompress(&self, hint: &Scalar) -> UniPoly { let mut linear_term = *hint - self.coeffs_except_linear_term[0] - self.coeffs_except_linear_term[0]; @@ -141,8 +149,9 @@ impl AbsorbInRO2Trait for UniPoly { } } -// This code is based on code from https://github.com/a16z/jolt/blob/main/jolt-core/src/utils/gaussian_elimination.rs, which itself is -// inspired by https://github.com/TheAlgorithms/Rust/blob/master/src/math/gaussian_elimination.rs +/// Performs Gaussian elimination on the given augmented matrix to solve a system of linear equations. +/// This code is based on code from https://github.com/a16z/jolt/blob/main/jolt-core/src/utils/gaussian_elimination.rs, +/// which itself is inspired by https://github.com/TheAlgorithms/Rust/blob/master/src/math/gaussian_elimination.rs pub fn gaussian_elimination(matrix: &mut [Vec]) -> Vec { let size = matrix.len(); assert_eq!(size, matrix[0].len() - 1); From 80241cd97996a5a6d5a3404a8150f9917dbde7c9 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Tue, 6 Jan 2026 16:27:40 -0800 Subject: [PATCH 11/24] Add EVM-compatible serialization support - Add evm feature flag - Create evm_serde.rs with CustomSerdeTrait and EvmCompatSerde - Add CustomSerdeTrait implementations for all curve types via macro - Add serde_as annotations to R1CS, Spartan, and HyperKZG types - Support big-endian serialization for EVM compatibility --- Cargo.toml | 2 + src/provider/bn256_grumpkin.rs | 52 ++++++++++++++++++++++ src/provider/hyperkzg.rs | 10 +++++ src/provider/traits.rs | 78 ++++++++++++++++++++++++++++++++- src/r1cs/mod.rs | 7 +++ src/spartan/direct.rs | 4 ++ src/spartan/polys/identity.rs | 8 ++-- src/spartan/polys/univariate.rs | 16 ++++--- src/spartan/ppsnark.rs | 24 ++++++++++ src/traits/evm_serde.rs | 39 +++++++++++++++++ src/traits/mod.rs | 7 ++- 11 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 src/traits/evm_serde.rs diff --git a/Cargo.toml b/Cargo.toml index e88bb7f0d..f4a4945a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ num-bigint = { version = "0.4.6", features = ["serde", "rand"] } num-traits = "0.2.19" num-integer = "0.1.46" serde = { version = "1.0.217", features = ["derive"] } +serde_with = "3.8.3" bincode = { version = "2", features = ["serde", "std"] } bitvec = "1.0" blitzar = { version = "4.4.2", optional = true } @@ -75,3 +76,4 @@ harness = false default = ["halo2curves/asm"] flamegraph = ["pprof2/flamegraph", "pprof2/criterion"] experimental = [] +evm = [] diff --git a/src/provider/bn256_grumpkin.rs b/src/provider/bn256_grumpkin.rs index ccc90911f..0f38b4ef6 100644 --- a/src/provider/bn256_grumpkin.rs +++ b/src/provider/bn256_grumpkin.rs @@ -160,3 +160,55 @@ impl TranscriptReprTrait for G2Affine { unimplemented!() } } + +// CustomSerdeTrait implementations for G2 +impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { + #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { + use crate::traits::evm_serde::EvmCompatSerde; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + + #[serde_as] + #[derive(Deserialize, Serialize)] + struct HelperBase( + #[serde_as(as = "EvmCompatSerde")] bn256::Base, + #[serde_as(as = "EvmCompatSerde")] bn256::Base, + ); + + #[derive(Deserialize, Serialize)] + struct HelperAffine(HelperBase, HelperBase); + + let affine = HelperAffine( + HelperBase(*self.x.c0(), *self.x.c1()), + HelperBase(*self.y.c0(), *self.y.c1()), + ); + + affine.serialize(serializer) + } + + #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { + use crate::traits::evm_serde::EvmCompatSerde; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + use halo2curves::bn256::Fq2; + + #[serde_as] + #[derive(Deserialize, Serialize)] + struct HelperBase( + #[serde_as(as = "EvmCompatSerde")] bn256::Base, + #[serde_as(as = "EvmCompatSerde")] bn256::Base, + ); + + #[derive(Deserialize, Serialize)] + struct HelperAffine(HelperBase, HelperBase); + + let affine = HelperAffine::deserialize(deserializer)?; + + Ok(G2Affine { + x: Fq2::new(affine.0.0, affine.0.1), + y: Fq2::new(affine.1.0, affine.1.1), + }) + } +} diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index 1f82d3024..1d57e2c7f 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -34,6 +34,8 @@ use num_traits::ToPrimitive; use rand_core::OsRng; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use crate::traits::evm_serde::EvmCompatSerde; /// Alias to points on G1 that are in preprocessed form type G1Affine = <::GE as DlogGroup>::AffineGroupElement; @@ -611,26 +613,34 @@ pub struct ProverKey { } /// A verifier key +#[serde_as] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "")] pub struct VerifierKey where E::GE: PairingGroup, { + #[serde_as(as = "EvmCompatSerde")] pub(crate) G: G1Affine, + #[serde_as(as = "EvmCompatSerde")] pub(crate) H: G2Affine, + #[serde_as(as = "EvmCompatSerde")] pub(crate) tau_H: G2Affine, } /// Provides an implementation of a polynomial evaluation argument +#[serde_as] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(bound = "")] pub struct EvaluationArgument where E::GE: PairingGroup, { + #[serde_as(as = "Vec")] com: Vec>, + #[serde_as(as = "[EvmCompatSerde; 3]")] w: [G1Affine; 3], + #[serde_as(as = "Vec<[EvmCompatSerde; 3]>")] v: Vec<[E::Scalar; 3]>, } diff --git a/src/provider/traits.rs b/src/provider/traits.rs index f346c4902..17494a9d1 100644 --- a/src/provider/traits.rs +++ b/src/provider/traits.rs @@ -49,7 +49,8 @@ pub trait DlogGroup: + for<'de> Deserialize<'de> + TranscriptReprTrait + CurveAffine - + SerdeObject; + + SerdeObject + + crate::traits::evm_serde::CustomSerdeTrait; /// Produce a vector of group elements using a static label fn from_label(label: &'static [u8], n: usize) -> Vec; @@ -150,6 +151,81 @@ macro_rules! impl_traits_no_dlog_ext { } } + impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Scalar { + #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { + use ff::PrimeField; + use serde::Serialize; + let mut bytes = self.to_repr(); + bytes.as_mut().reverse(); // big-endian + bytes.serialize(serializer) + } + + #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { + use ff::PrimeField; + use serde::Deserialize; + use serde::de::Error; + let mut bytes = <[u8; 32]>::deserialize(deserializer)?; + bytes.reverse(); // big-endian + Option::from(Self::from_repr(bytes.into())) + .ok_or_else(|| D::Error::custom("deserialized bytes don't encode a valid field element")) + } + } + + impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Affine { + #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { + use serde_with::serde_as; + use $crate::traits::evm_serde::EvmCompatSerde; + use serde::{Deserialize, Serialize}; + + #[serde_as] + #[derive(Deserialize, Serialize)] + struct HelperAffine( + #[serde_as(as = "EvmCompatSerde")] $name::Base, + #[serde_as(as = "EvmCompatSerde")] $name::Base, + ); + + let affine = HelperAffine(self.x, self.y); + affine.serialize(serializer) + } + + #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { + use serde_with::serde_as; + use $crate::traits::evm_serde::EvmCompatSerde; + use serde::{Deserialize, Serialize}; + + #[serde_as] + #[derive(Deserialize, Serialize)] + struct HelperAffine( + #[serde_as(as = "EvmCompatSerde")] $name::Base, + #[serde_as(as = "EvmCompatSerde")] $name::Base, + ); + + let affine = HelperAffine::deserialize(deserializer)?; + Ok($name::Affine { + x: affine.0, + y: affine.1, + }) + } + } + + impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Point { + #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { + use $crate::traits::evm_serde::CustomSerdeTrait; + <$name::Affine as CustomSerdeTrait>::serialize(&self.to_affine(), serializer) + } + + #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { + use $crate::traits::evm_serde::CustomSerdeTrait; + Ok(Self::from(<$name::Affine as CustomSerdeTrait>::deserialize(deserializer)?)) + } + } + impl DlogGroup for $name::Point { type AffineGroupElement = $name::Affine; diff --git a/src/r1cs/mod.rs b/src/r1cs/mod.rs index 1a2d28096..b8198ab94 100644 --- a/src/r1cs/mod.rs +++ b/src/r1cs/mod.rs @@ -19,6 +19,8 @@ use once_cell::sync::OnceCell; use rand_core::OsRng; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use crate::traits::evm_serde::EvmCompatSerde; mod sparse; pub use sparse::SparseMatrix; @@ -46,10 +48,12 @@ pub struct R1CSWitness { } /// A type that holds an R1CS instance +#[serde_as] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] pub struct R1CSInstance { pub(crate) comm_W: Commitment, + #[serde_as(as = "Vec")] pub(crate) X: Vec, } @@ -63,12 +67,15 @@ pub struct RelaxedR1CSWitness { } /// A type that holds a Relaxed R1CS instance +#[serde_as] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] pub struct RelaxedR1CSInstance { pub(crate) comm_W: Commitment, pub(crate) comm_E: Commitment, + #[serde_as(as = "Vec")] pub(crate) X: Vec, + #[serde_as(as = "EvmCompatSerde")] pub(crate) u: E::Scalar, } diff --git a/src/spartan/direct.rs b/src/spartan/direct.rs index 31b6b004c..0d21896b8 100644 --- a/src/spartan/direct.rs +++ b/src/spartan/direct.rs @@ -22,6 +22,8 @@ use crate::{ use core::marker::PhantomData; use ff::Field; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use crate::traits::evm_serde::EvmCompatSerde; /// A direct circuit that can be synthesized pub struct DirectCircuit> { @@ -98,6 +100,7 @@ impl> VerifierKey { } /// A direct SNARK proving a step circuit +#[serde_as] #[derive(Clone, Serialize, Deserialize)] #[serde(bound = "")] pub struct DirectSNARK @@ -107,6 +110,7 @@ where C: StepCircuit, { comm_W: Commitment, // commitment to the witness + #[serde_as(as = "EvmCompatSerde")] blind_r_W: E::Scalar, snark: S, // snark proving the witness is satisfying _p: PhantomData, diff --git a/src/spartan/polys/identity.rs b/src/spartan/polys/identity.rs index 5fcb3ac1f..54d43ba71 100644 --- a/src/spartan/polys/identity.rs +++ b/src/spartan/polys/identity.rs @@ -1,14 +1,16 @@ +//! Identity polynomial implementation. + use core::marker::PhantomData; use ff::PrimeField; -/// Represents an identity polynomial that maps a point to its index in binary representation. +/// A polynomial that evaluates to the identity of its input pub struct IdentityPolynomial { ell: usize, _p: PhantomData, } impl IdentityPolynomial { - /// Creates a new identity polynomial with the specified number of variables. + /// Creates a new identity polynomial with the given number of variables pub fn new(ell: usize) -> Self { IdentityPolynomial { ell, @@ -16,7 +18,7 @@ impl IdentityPolynomial { } } - /// Evaluates the identity polynomial at the given point. + /// Evaluates the polynomial at the given point pub fn evaluate(&self, r: &[Scalar]) -> Scalar { assert_eq!(self.ell, r.len()); let mut power_of_two = 1_u64; diff --git a/src/spartan/polys/univariate.rs b/src/spartan/polys/univariate.rs index 92c0d2ed9..be982a415 100644 --- a/src/spartan/polys/univariate.rs +++ b/src/spartan/polys/univariate.rs @@ -1,11 +1,15 @@ //! Main components: //! - `UniPoly`: an univariate dense polynomial in coefficient form (big endian), //! - `CompressedUniPoly`: a univariate dense polynomial, compressed (omitted linear term), in coefficient form (little endian), -use crate::traits::{AbsorbInRO2Trait, Engine, Group, ROTrait, TranscriptReprTrait}; +use crate::traits::{ + evm_serde::{CustomSerdeTrait, EvmCompatSerde}, + AbsorbInRO2Trait, Engine, Group, ROTrait, TranscriptReprTrait, +}; use core::panic; use ff::PrimeField; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; /// A univariate dense polynomial in coefficient form (little endian). /// For example, ax^2 + bx + c is stored as vec![c, b, a] @@ -18,12 +22,14 @@ pub struct UniPoly { /// A compressed univariate polynomial with the linear term omitted (little endian). /// For example, ax^2 + bx + c is stored as vec![c, a] /// and ax^3 + bx^2 + cx + d is stored as vec![d, c, a] +#[serde_as] #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CompressedUniPoly { +pub struct CompressedUniPoly { + #[serde_as(as = "Vec")] coeffs_except_linear_term: Vec, } -impl UniPoly { +impl UniPoly { #[cfg(feature = "experimental")] pub fn from_evals(evals: &[Scalar]) -> Self { let n = evals.len(); @@ -111,7 +117,7 @@ impl UniPoly { } } -impl CompressedUniPoly { +impl CompressedUniPoly { /// Decompresses the polynomial by recovering the linear term using the hint. /// We require eval(0) + eval(1) = hint, so we can solve for the linear term as: /// linear_term = hint - 2 * constant_term - deg2 term - deg3 term @@ -131,7 +137,7 @@ impl CompressedUniPoly { } } -impl TranscriptReprTrait for UniPoly { +impl TranscriptReprTrait for UniPoly where G::Scalar: CustomSerdeTrait { fn to_transcript_bytes(&self) -> Vec { let coeffs = self.compress().coeffs_except_linear_term; coeffs diff --git a/src/spartan/ppsnark.rs b/src/spartan/ppsnark.rs index 13e0a6f11..b1f7bced6 100644 --- a/src/spartan/ppsnark.rs +++ b/src/spartan/ppsnark.rs @@ -34,6 +34,8 @@ use itertools::Itertools as _; use once_cell::sync::OnceCell; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use crate::traits::evm_serde::EvmCompatSerde; fn padded(v: &[E::Scalar], n: usize, e: &E::Scalar) -> Vec { let mut v_padded = vec![*e; n]; @@ -792,6 +794,7 @@ impl> SimpleDigestible for VerifierKey> { @@ -810,34 +813,55 @@ pub struct RelaxedR1CSSNARK> { comm_w_plus_r_inv_col: Commitment, // claims about Az, Bz, and Cz polynomials + #[serde_as(as = "EvmCompatSerde")] eval_Az_at_tau: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_Bz_at_tau: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_Cz_at_tau: E::Scalar, // sum-check sc: SumcheckProof, // claims from the end of sum-check + #[serde_as(as = "EvmCompatSerde")] eval_Az: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_Bz: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_Cz: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_E: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_L_row: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_L_col: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_val_A: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_val_B: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_val_C: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_W: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_t_plus_r_inv_row: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_row: E::Scalar, // address + #[serde_as(as = "EvmCompatSerde")] eval_w_plus_r_inv_row: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_ts_row: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_t_plus_r_inv_col: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_col: E::Scalar, // address + #[serde_as(as = "EvmCompatSerde")] eval_w_plus_r_inv_col: E::Scalar, + #[serde_as(as = "EvmCompatSerde")] eval_ts_col: E::Scalar, // a PCS evaluation argument diff --git a/src/traits/evm_serde.rs b/src/traits/evm_serde.rs new file mode 100644 index 000000000..859b1c35e --- /dev/null +++ b/src/traits/evm_serde.rs @@ -0,0 +1,39 @@ +//! This module defines a trait that with serde_with::serde_as crate, +//! defines the behavior of a customized (de)serializer +//! to override the default ones from serde crate. + +use serde::{Deserializer, Serializer}; +use serde_with::{DeserializeAs, SerializeAs}; + +/// A helper trait to implement serde with custom behavior +pub trait CustomSerdeTrait: Sized + serde::Serialize + for<'de> serde::Deserialize<'de> { + /// customized serializer, default to original serializer from serde + fn serialize(&self, serializer: S) -> Result { + serde::Serialize::serialize(&self, serializer) + } + /// customized deserializer, default to original deserializer from serde + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { + ::deserialize(deserializer) + } +} + +/// A struct as helper for serde_as to use customized (de)serializer defined by CustomSerdeTrait +pub struct EvmCompatSerde; + +impl SerializeAs for EvmCompatSerde { + fn serialize_as(source: &T, serializer: S) -> Result + where + S: Serializer, + { + ::serialize(source, serializer) + } +} + +impl<'de, T: CustomSerdeTrait> DeserializeAs<'de, T> for EvmCompatSerde { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + ::deserialize(deserializer) + } +} diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 5a0e97e45..84a887012 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -9,6 +9,8 @@ use num_bigint::BigInt; use serde::{Deserialize, Serialize}; pub mod commitment; +pub mod evm_serde; +pub use evm_serde::CustomSerdeTrait; use commitment::CommitmentEngineTrait; @@ -28,7 +30,7 @@ pub trait Group: Clone + Copy + Debug + Send + Sync + Sized + Eq + PartialEq { /// A collection of engines that are required by the library pub trait Engine: Clone + Copy + Debug + Send + Sync + Sized + Eq + PartialEq { /// A type representing an element of the base field of the group - type Base: PrimeFieldBits + TranscriptReprTrait + Serialize + for<'de> Deserialize<'de>; + type Base: PrimeFieldBits + TranscriptReprTrait + Serialize + for<'de> Deserialize<'de> + CustomSerdeTrait; /// A type representing an element of the scalar field of the group type Scalar: PrimeFieldBits @@ -37,7 +39,8 @@ pub trait Engine: Clone + Copy + Debug + Send + Sync + Sized + Eq + PartialEq { + Sync + TranscriptReprTrait + Serialize - + for<'de> Deserialize<'de>; + + for<'de> Deserialize<'de> + + CustomSerdeTrait; /// A type that represents an element of the group type GE: Group + Serialize + for<'de> Deserialize<'de>; From a08ababd4ffcc670ad3754d7438c3fa4f16a3403 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Tue, 6 Jan 2026 20:44:22 -0800 Subject: [PATCH 12/24] serialization changes --- src/provider/bn256_grumpkin.rs | 6 ++--- src/provider/hyperkzg.rs | 2 +- src/provider/keccak.rs | 40 +++++++++++++++++++++++++++++++-- src/provider/traits.rs | 10 +++++---- src/r1cs/mod.rs | 2 +- src/spartan/direct.rs | 2 +- src/spartan/polys/univariate.rs | 5 ++++- src/spartan/ppsnark.rs | 2 +- src/traits/mod.rs | 6 ++++- 9 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/provider/bn256_grumpkin.rs b/src/provider/bn256_grumpkin.rs index 0f38b4ef6..5edc9237a 100644 --- a/src/provider/bn256_grumpkin.rs +++ b/src/provider/bn256_grumpkin.rs @@ -190,9 +190,9 @@ impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use crate::traits::evm_serde::EvmCompatSerde; + use halo2curves::bn256::Fq2; use serde::{Deserialize, Serialize}; use serde_with::serde_as; - use halo2curves::bn256::Fq2; #[serde_as] #[derive(Deserialize, Serialize)] @@ -207,8 +207,8 @@ impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { let affine = HelperAffine::deserialize(deserializer)?; Ok(G2Affine { - x: Fq2::new(affine.0.0, affine.0.1), - y: Fq2::new(affine.1.0, affine.1.1), + x: Fq2::new(affine.0 .0, affine.0 .1), + y: Fq2::new(affine.1 .0, affine.1 .1), }) } } diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index 1d57e2c7f..368e25a9d 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -6,6 +6,7 @@ //! (2) HyperKZG is specialized to use KZG as the univariate commitment scheme, so it includes several optimizations (both during the transformation of multilinear-to-univariate claims //! and within the KZG commitment scheme implementation itself). #![allow(non_snake_case)] +use crate::traits::evm_serde::EvmCompatSerde; use crate::{ errors::NovaError, gadgets::utils::to_bignat_repr, @@ -35,7 +36,6 @@ use rand_core::OsRng; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::traits::evm_serde::EvmCompatSerde; /// Alias to points on G1 that are in preprocessed form type G1Affine = <::GE as DlogGroup>::AffineGroupElement; diff --git a/src/provider/keccak.rs b/src/provider/keccak.rs index 48cf50fbd..fe355b804 100644 --- a/src/provider/keccak.rs +++ b/src/provider/keccak.rs @@ -37,11 +37,18 @@ fn compute_updated_state(keccak_instance: Keccak256, input: &[u8]) -> [u8; KECCA let output_lo = hasher_lo.finalize(); let output_hi = hasher_hi.finalize(); - [output_lo, output_hi] + #[cfg(not(feature = "evm"))] + return [output_lo, output_hi] .concat() .as_slice() .try_into() - .unwrap() + .unwrap(); + #[cfg(feature = "evm")] + return [output_hi, output_lo] + .concat() + .as_slice() + .try_into() + .unwrap(); } impl TranscriptEngineTrait for Keccak256Transcript { @@ -58,6 +65,7 @@ impl TranscriptEngineTrait for Keccak256Transcript { } } + #[cfg(not(feature = "evm"))] fn squeeze(&mut self, label: &'static [u8]) -> Result { // we gather the full input from the round, preceded by the current state of the transcript let input = [ @@ -84,6 +92,34 @@ impl TranscriptEngineTrait for Keccak256Transcript { Ok(E::Scalar::from_uniform(&output)) } + #[cfg(feature = "evm")] + fn squeeze(&mut self, label: &'static [u8]) -> Result { + // we gather the full input from the round, preceded by the current state of the transcript + let input = [ + DOM_SEP_TAG, + self.round.to_be_bytes().as_ref(), + self.state.as_ref(), + label, + ] + .concat(); + let mut output = compute_updated_state(self.transcript.clone(), &input); + + // update state + self.round = { + if let Some(v) = self.round.checked_add(1) { + v + } else { + return Err(NovaError::InternalTranscriptError); + } + }; + self.state.copy_from_slice(&output); + self.transcript = Keccak256::new(); + + // squeeze out a challenge + output.reverse(); + Ok(E::Scalar::from_uniform(&output)) + } + fn absorb>(&mut self, label: &'static [u8], o: &T) { self.transcript.update(label); self.transcript.update(o.to_transcript_bytes()); diff --git a/src/provider/traits.rs b/src/provider/traits.rs index 17494a9d1..3b3931f42 100644 --- a/src/provider/traits.rs +++ b/src/provider/traits.rs @@ -164,8 +164,8 @@ macro_rules! impl_traits_no_dlog_ext { #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use ff::PrimeField; - use serde::Deserialize; use serde::de::Error; + use serde::Deserialize; let mut bytes = <[u8; 32]>::deserialize(deserializer)?; bytes.reverse(); // big-endian Option::from(Self::from_repr(bytes.into())) @@ -176,9 +176,9 @@ macro_rules! impl_traits_no_dlog_ext { impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Affine { #[cfg(feature = "evm")] fn serialize(&self, serializer: S) -> Result { + use serde::{Deserialize, Serialize}; use serde_with::serde_as; use $crate::traits::evm_serde::EvmCompatSerde; - use serde::{Deserialize, Serialize}; #[serde_as] #[derive(Deserialize, Serialize)] @@ -193,9 +193,9 @@ macro_rules! impl_traits_no_dlog_ext { #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { + use serde::{Deserialize, Serialize}; use serde_with::serde_as; use $crate::traits::evm_serde::EvmCompatSerde; - use serde::{Deserialize, Serialize}; #[serde_as] #[derive(Deserialize, Serialize)] @@ -222,7 +222,9 @@ macro_rules! impl_traits_no_dlog_ext { #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use $crate::traits::evm_serde::CustomSerdeTrait; - Ok(Self::from(<$name::Affine as CustomSerdeTrait>::deserialize(deserializer)?)) + Ok(Self::from( + <$name::Affine as CustomSerdeTrait>::deserialize(deserializer)?, + )) } } diff --git a/src/r1cs/mod.rs b/src/r1cs/mod.rs index b8198ab94..c4cf7a1b5 100644 --- a/src/r1cs/mod.rs +++ b/src/r1cs/mod.rs @@ -1,4 +1,5 @@ //! This module defines R1CS related types and a folding scheme for Relaxed R1CS +use crate::traits::evm_serde::EvmCompatSerde; use crate::{ constants::{BN_LIMB_WIDTH, BN_N_LIMBS}, digest::{DigestComputer, SimpleDigestible}, @@ -20,7 +21,6 @@ use rand_core::OsRng; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::traits::evm_serde::EvmCompatSerde; mod sparse; pub use sparse::SparseMatrix; diff --git a/src/spartan/direct.rs b/src/spartan/direct.rs index 0d21896b8..05a562746 100644 --- a/src/spartan/direct.rs +++ b/src/spartan/direct.rs @@ -1,6 +1,7 @@ //! This module provides interfaces to directly prove a step circuit by using Spartan SNARK. //! In particular, it supports any SNARK that implements `RelaxedR1CSSNARK` trait //! (e.g., with the SNARKs implemented in ppsnark.rs or snark.rs). +use crate::traits::evm_serde::EvmCompatSerde; use crate::{ errors::NovaError, frontend::{ @@ -23,7 +24,6 @@ use core::marker::PhantomData; use ff::Field; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::traits::evm_serde::EvmCompatSerde; /// A direct circuit that can be synthesized pub struct DirectCircuit> { diff --git a/src/spartan/polys/univariate.rs b/src/spartan/polys/univariate.rs index be982a415..87c117c8f 100644 --- a/src/spartan/polys/univariate.rs +++ b/src/spartan/polys/univariate.rs @@ -137,7 +137,10 @@ impl CompressedUniPoly { } } -impl TranscriptReprTrait for UniPoly where G::Scalar: CustomSerdeTrait { +impl TranscriptReprTrait for UniPoly +where + G::Scalar: CustomSerdeTrait, +{ fn to_transcript_bytes(&self) -> Vec { let coeffs = self.compress().coeffs_except_linear_term; coeffs diff --git a/src/spartan/ppsnark.rs b/src/spartan/ppsnark.rs index b1f7bced6..24e9c5610 100644 --- a/src/spartan/ppsnark.rs +++ b/src/spartan/ppsnark.rs @@ -3,6 +3,7 @@ //! The verifier in this preprocessing SNARK maintains a commitment to R1CS matrices. This is beneficial when using a //! polynomial commitment scheme in which the verifier's costs is succinct. //! The SNARK implemented here is described in the MicroNova paper. +use crate::traits::evm_serde::EvmCompatSerde; use crate::{ digest::{DigestComputer, SimpleDigestible}, errors::NovaError, @@ -35,7 +36,6 @@ use once_cell::sync::OnceCell; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::traits::evm_serde::EvmCompatSerde; fn padded(v: &[E::Scalar], n: usize, e: &E::Scalar) -> Vec { let mut v_padded = vec![*e; n]; diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 84a887012..ded4320d7 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -30,7 +30,11 @@ pub trait Group: Clone + Copy + Debug + Send + Sync + Sized + Eq + PartialEq { /// A collection of engines that are required by the library pub trait Engine: Clone + Copy + Debug + Send + Sync + Sized + Eq + PartialEq { /// A type representing an element of the base field of the group - type Base: PrimeFieldBits + TranscriptReprTrait + Serialize + for<'de> Deserialize<'de> + CustomSerdeTrait; + type Base: PrimeFieldBits + + TranscriptReprTrait + + Serialize + + for<'de> Deserialize<'de> + + CustomSerdeTrait; /// A type representing an element of the scalar field of the group type Scalar: PrimeFieldBits From 054cca28cc38896e5311a9e2c78723023fd51e85 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 7 Jan 2026 10:20:05 -0800 Subject: [PATCH 13/24] Add EvmCompatSerde to Commitment and fix tests for evm feature - Add EvmCompatSerde wrapper to Commitment.comm for EVM-compatible serialization - Add CustomSerdeTrait bound to Engine::GE type - Update univariate poly tests to include CustomSerdeTrait bound - Skip keccak transcript tests when evm feature is enabled (different byte order) - Skip hyperkzg_small test with evm feature (different serialization size) --- src/provider/hyperkzg.rs | 1 + src/provider/keccak.rs | 9 +++++++++ src/provider/pedersen.rs | 4 ++++ src/spartan/polys/univariate.rs | 4 ++-- src/traits/mod.rs | 2 +- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index 163326a07..c56a50873 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -1125,6 +1125,7 @@ mod tests { } #[test] + #[cfg(not(feature = "evm"))] fn test_hyperkzg_small() { let n = 4; diff --git a/src/provider/keccak.rs b/src/provider/keccak.rs index fe355b804..da96b9410 100644 --- a/src/provider/keccak.rs +++ b/src/provider/keccak.rs @@ -132,6 +132,7 @@ impl TranscriptEngineTrait for Keccak256Transcript { } #[cfg(test)] +#[allow(unused_imports)] mod tests { use crate::{ provider::{ @@ -144,6 +145,7 @@ mod tests { use rand::Rng; use sha3::{Digest, Keccak256}; + #[cfg(not(feature = "evm"))] fn test_keccak_transcript_with(expected_h1: &'static str, expected_h2: &'static str) { let mut transcript: Keccak256Transcript = Keccak256Transcript::new(b"test"); @@ -171,6 +173,7 @@ mod tests { } #[test] + #[cfg(not(feature = "evm"))] fn test_keccak_transcript() { test_keccak_transcript_with::( "b67339da79ce5f6dc72ad23c8c3b4179f49655cadf92d47e79c3e7788f00f125", @@ -199,11 +202,13 @@ mod tests { ); } + #[allow(unused)] use super::{ DOM_SEP_TAG, KECCAK256_PREFIX_CHALLENGE_HI, KECCAK256_PREFIX_CHALLENGE_LO, KECCAK256_STATE_SIZE, PERSONA_TAG, }; + #[cfg(not(feature = "evm"))] fn compute_updated_state_for_testing(input: &[u8]) -> [u8; KECCAK256_STATE_SIZE] { let input_lo = [input, &[KECCAK256_PREFIX_CHALLENGE_LO]].concat(); let input_hi = [input, &[KECCAK256_PREFIX_CHALLENGE_HI]].concat(); @@ -224,6 +229,7 @@ mod tests { .unwrap() } + #[cfg(not(feature = "evm"))] fn squeeze_for_testing( transcript: &[u8], round: u16, @@ -243,6 +249,7 @@ mod tests { // This test is meant to ensure compatibility between the incremental way of computing the transcript above, and // the former, which materialized the entirety of the input vector before calling Keccak256 on it. + #[cfg(not(feature = "evm"))] fn test_keccak_transcript_incremental_vs_explicit_with() { let test_label = b"test"; let mut transcript: Keccak256Transcript = Keccak256Transcript::new(test_label); @@ -278,6 +285,7 @@ mod tests { } #[test] + #[cfg(not(feature = "evm"))] fn test_keccak_transcript_incremental_vs_explicit() { test_keccak_transcript_incremental_vs_explicit_with::(); test_keccak_transcript_incremental_vs_explicit_with::(); @@ -287,3 +295,4 @@ mod tests { test_keccak_transcript_incremental_vs_explicit_with::(); } } + diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index 946b85646..d9df7c42a 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -20,6 +20,8 @@ use num_integer::Integer; use num_traits::ToPrimitive; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use crate::traits::evm_serde::EvmCompatSerde; #[cfg(feature = "io")] const KEY_FILE_HEAD: [u8; 12] = *b"PEDERSEN_KEY"; @@ -53,9 +55,11 @@ where } /// A type that holds a commitment +#[serde_as] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] pub struct Commitment { + #[serde_as(as = "EvmCompatSerde")] pub(crate) comm: E::GE, } diff --git a/src/spartan/polys/univariate.rs b/src/spartan/polys/univariate.rs index d922a3915..68aea75ce 100644 --- a/src/spartan/polys/univariate.rs +++ b/src/spartan/polys/univariate.rs @@ -227,7 +227,7 @@ mod tests { use super::*; use crate::provider::{bn256_grumpkin::bn256, pasta::pallas, secp_secq::secp256k1}; - fn test_from_evals_quad_with() { + fn test_from_evals_quad_with() { // polynomial is 2x^2 + 3x + 1 let e0 = F::ONE; let e1 = F::from(6); @@ -260,7 +260,7 @@ mod tests { test_from_evals_quad_with::(); } - fn test_from_evals_cubic_with() { + fn test_from_evals_cubic_with() { // polynomial is x^3 + 2x^2 + 3x + 1 let e0 = F::ONE; // f(0) let e1 = F::from(7); // f(1) diff --git a/src/traits/mod.rs b/src/traits/mod.rs index ded4320d7..913532a5a 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -47,7 +47,7 @@ pub trait Engine: Clone + Copy + Debug + Send + Sync + Sized + Eq + PartialEq { + CustomSerdeTrait; /// A type that represents an element of the group - type GE: Group + Serialize + for<'de> Deserialize<'de>; + type GE: Group + Serialize + for<'de> Deserialize<'de> + CustomSerdeTrait; /// A type that represents a circuit-friendly sponge that consumes /// elements from the base field From de9adb796821c9fa5ba956fb90762d75fed633c2 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 7 Jan 2026 10:20:05 -0800 Subject: [PATCH 14/24] Add EvmCompatSerde to Commitment and fix tests for evm feature - Add EvmCompatSerde wrapper to Commitment.comm for EVM-compatible serialization - Add CustomSerdeTrait bound to Engine::GE type - Update univariate poly tests to include CustomSerdeTrait bound - Skip keccak transcript tests when evm feature is enabled (different byte order) - Skip hyperkzg_small test with evm feature (different serialization size) --- src/provider/pedersen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index d9df7c42a..d301e5a6b 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -322,6 +322,7 @@ where fn ck_to_coordinates(ck: &Self::CommitmentKey) -> Vec<(E::Base, E::Base)> { ck.to_coordinates() + } #[cfg(feature = "io")] fn save_setup( From 494ebb591326eb66ffde88032e482862aec8dd34 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 7 Jan 2026 10:59:34 -0800 Subject: [PATCH 15/24] cargo fmt --- src/provider/hyperkzg.rs | 2 +- src/provider/keccak.rs | 1 - src/provider/pedersen.rs | 4 ++-- src/traits/mod.rs | 5 ++++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index c56a50873..4ab7bb9a8 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -10,9 +10,9 @@ use crate::provider::{ptau::PtauFileError, read_ptau, write_ptau}; use crate::{ errors::NovaError, - traits::evm_serde::EvmCompatSerde, gadgets::utils::to_bignat_repr, provider::traits::{DlogGroup, DlogGroupExt, PairingGroup}, + traits::evm_serde::EvmCompatSerde, traits::{ commitment::{CommitmentEngineTrait, CommitmentTrait, Len}, evaluation::EvaluationEngineTrait, diff --git a/src/provider/keccak.rs b/src/provider/keccak.rs index da96b9410..4f35b6fde 100644 --- a/src/provider/keccak.rs +++ b/src/provider/keccak.rs @@ -295,4 +295,3 @@ mod tests { test_keccak_transcript_incremental_vs_explicit_with::(); } } - diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index d301e5a6b..871b3c44a 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -1,6 +1,7 @@ //! This module provides an implementation of a commitment engine #[cfg(feature = "io")] use crate::provider::ptau::{read_points, write_points, PtauFileError}; +use crate::traits::evm_serde::EvmCompatSerde; use crate::{ errors::NovaError, gadgets::utils::to_bignat_repr, @@ -21,7 +22,6 @@ use num_traits::ToPrimitive; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::traits::evm_serde::EvmCompatSerde; #[cfg(feature = "io")] const KEY_FILE_HEAD: [u8; 12] = *b"PEDERSEN_KEY"; @@ -323,7 +323,7 @@ where fn ck_to_coordinates(ck: &Self::CommitmentKey) -> Vec<(E::Base, E::Base)> { ck.to_coordinates() } - + #[cfg(feature = "io")] fn save_setup( ck: &Self::CommitmentKey, diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 913532a5a..579b2b22c 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -47,7 +47,10 @@ pub trait Engine: Clone + Copy + Debug + Send + Sync + Sized + Eq + PartialEq { + CustomSerdeTrait; /// A type that represents an element of the group - type GE: Group + Serialize + for<'de> Deserialize<'de> + CustomSerdeTrait; + type GE: Group + + Serialize + + for<'de> Deserialize<'de> + + CustomSerdeTrait; /// A type that represents a circuit-friendly sponge that consumes /// elements from the base field From 7b5095f58f3e6f5f83b03b493f6f9361fe4a193b Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 7 Jan 2026 12:29:54 -0800 Subject: [PATCH 16/24] Always use EVM-compatible serialization for G1/G2 points - Remove all #[cfg(feature = "evm")] guards from CustomSerdeTrait impls - G1 points now always serialize as 64 bytes (x,y uncompressed) - G2 points now always serialize as 128 bytes - Keccak256 transcript always uses big-endian byte order - This ensures cross-crate serialization consistency --- examples/test_ser.rs | 33 ++++++ src/provider/bn256_grumpkin.rs | 4 +- src/provider/keccak.rs | 192 +-------------------------------- src/provider/pedersen.rs | 21 ++++ src/provider/traits.rs | 12 +-- 5 files changed, 67 insertions(+), 195 deletions(-) create mode 100644 examples/test_ser.rs diff --git a/examples/test_ser.rs b/examples/test_ser.rs new file mode 100644 index 000000000..6b22cfa2b --- /dev/null +++ b/examples/test_ser.rs @@ -0,0 +1,33 @@ +// Quick test to check serialization size of R1CSInstance +use nova_snark::provider::Bn256EngineKZG; +use nova_snark::traits::Engine; +use nova_snark::r1cs::R1CSInstance; +use halo2curves::group::Group; +use halo2curves::bn256::Fr; +use ff::Field; + +type E = Bn256EngineKZG; + +fn main() { + // Create an R1CSInstance with 3 elements in X + let comm_w = ::GE::generator(); + let x = vec![Fr::ONE, Fr::from(2), Fr::from(3)]; + + let instance = R1CSInstance::::new_unchecked(comm_w, x); + + // Serialize with bincode + let config = bincode::config::standard() + .with_big_endian() + .with_fixed_int_encoding(); + let bytes = bincode::serde::encode_to_vec(&instance, config).unwrap(); + + println!("Serialized R1CSInstance length: {} bytes", bytes.len()); + println!("Expected: 64 (G1) + 8 (len) + 3*32 (Fr) = 168 bytes"); + + // Print structure + println!("\nByte breakdown:"); + println!("Bytes 0-31 (should be G1.x): {:02x?}", &bytes[0..32]); + println!("Bytes 32-63 (should be G1.y): {:02x?}", &bytes[32..64]); + println!("Bytes 64-71 (should be len=3): {:02x?}", &bytes[64..72]); + println!("Bytes 72-103 (should be X[0]): {:02x?}", &bytes[72..104]); +} diff --git a/src/provider/bn256_grumpkin.rs b/src/provider/bn256_grumpkin.rs index 479c06fdb..83e38c04c 100644 --- a/src/provider/bn256_grumpkin.rs +++ b/src/provider/bn256_grumpkin.rs @@ -162,7 +162,7 @@ impl TranscriptReprTrait for G2Affine { // CustomSerdeTrait implementations for G2 impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { - #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { use crate::traits::evm_serde::EvmCompatSerde; use serde::{Deserialize, Serialize}; @@ -186,7 +186,7 @@ impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { affine.serialize(serializer) } - #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use crate::traits::evm_serde::EvmCompatSerde; use halo2curves::bn256::Fq2; diff --git a/src/provider/keccak.rs b/src/provider/keccak.rs index 4f35b6fde..d631f6012 100644 --- a/src/provider/keccak.rs +++ b/src/provider/keccak.rs @@ -37,18 +37,12 @@ fn compute_updated_state(keccak_instance: Keccak256, input: &[u8]) -> [u8; KECCA let output_lo = hasher_lo.finalize(); let output_hi = hasher_hi.finalize(); - #[cfg(not(feature = "evm"))] - return [output_lo, output_hi] + // EVM-compatible: big-endian order (hi, lo) + [output_hi, output_lo] .concat() .as_slice() .try_into() - .unwrap(); - #[cfg(feature = "evm")] - return [output_hi, output_lo] - .concat() - .as_slice() - .try_into() - .unwrap(); + .unwrap() } impl TranscriptEngineTrait for Keccak256Transcript { @@ -65,36 +59,9 @@ impl TranscriptEngineTrait for Keccak256Transcript { } } - #[cfg(not(feature = "evm"))] - fn squeeze(&mut self, label: &'static [u8]) -> Result { - // we gather the full input from the round, preceded by the current state of the transcript - let input = [ - DOM_SEP_TAG, - self.round.to_le_bytes().as_ref(), - self.state.as_ref(), - label, - ] - .concat(); - let output = compute_updated_state(self.transcript.clone(), &input); - - // update state - self.round = { - if let Some(v) = self.round.checked_add(1) { - v - } else { - return Err(NovaError::InternalTranscriptError); - } - }; - self.state.copy_from_slice(&output); - self.transcript = Keccak256::new(); - - // squeeze out a challenge - Ok(E::Scalar::from_uniform(&output)) - } - - #[cfg(feature = "evm")] fn squeeze(&mut self, label: &'static [u8]) -> Result { // we gather the full input from the round, preceded by the current state of the transcript + // EVM-compatible: big-endian byte order for round let input = [ DOM_SEP_TAG, self.round.to_be_bytes().as_ref(), @@ -115,7 +82,7 @@ impl TranscriptEngineTrait for Keccak256Transcript { self.state.copy_from_slice(&output); self.transcript = Keccak256::new(); - // squeeze out a challenge + // squeeze out a challenge (reverse for EVM compatibility) output.reverse(); Ok(E::Scalar::from_uniform(&output)) } @@ -132,65 +99,9 @@ impl TranscriptEngineTrait for Keccak256Transcript { } #[cfg(test)] -#[allow(unused_imports)] mod tests { - use crate::{ - provider::{ - keccak::Keccak256Transcript, Bn256EngineKZG, GrumpkinEngine, PallasEngine, Secp256k1Engine, - Secq256k1Engine, VestaEngine, - }, - traits::{Engine, PrimeFieldExt, TranscriptEngineTrait, TranscriptReprTrait}, - }; - use ff::PrimeField; - use rand::Rng; use sha3::{Digest, Keccak256}; - #[cfg(not(feature = "evm"))] - fn test_keccak_transcript_with(expected_h1: &'static str, expected_h2: &'static str) { - let mut transcript: Keccak256Transcript = Keccak256Transcript::new(b"test"); - - // two scalars - let s1 = ::Scalar::from(2u64); - let s2 = ::Scalar::from(5u64); - - // add the scalars to the transcript - transcript.absorb(b"s1", &s1); - transcript.absorb(b"s2", &s2); - - // make a challenge - let c1: ::Scalar = transcript.squeeze(b"c1").unwrap(); - assert_eq!(hex::encode(c1.to_repr().as_ref()), expected_h1); - - // a scalar - let s3 = ::Scalar::from(128u64); - - // add the scalar to the transcript - transcript.absorb(b"s3", &s3); - - // make a challenge - let c2: ::Scalar = transcript.squeeze(b"c2").unwrap(); - assert_eq!(hex::encode(c2.to_repr().as_ref()), expected_h2); - } - - #[test] - #[cfg(not(feature = "evm"))] - fn test_keccak_transcript() { - test_keccak_transcript_with::( - "b67339da79ce5f6dc72ad23c8c3b4179f49655cadf92d47e79c3e7788f00f125", - "b7f033d47b3519dd6efe320b995eaad1dc11712cb9b655d2e7006ed5f86bd321", - ); - - test_keccak_transcript_with::( - "b387ba3a8b9a22b3b7544a3dbbd26a048a1d354d8dc582c64d1513335e66a205", - "73ad65097d947fe45de5241bb340bbd97b198b52cc559a9657f73c361bf8700b", - ); - - test_keccak_transcript_with::( - "f15ddd8fa1675a9e273e0ef441711005d77a5fd485f4e6cdee59760ca01493fa", - "3c019f0e557abaecc99790382974cb27132bfe038af9c4d43a33ec9c426e19f5", - ); - } - #[test] fn test_keccak_example() { let mut hasher = Keccak256::new(); @@ -201,97 +112,4 @@ mod tests { "29045a592007d0c246ef02c2223570da9522d0cf0f73282c79a1bc8f0bb2c238" ); } - - #[allow(unused)] - use super::{ - DOM_SEP_TAG, KECCAK256_PREFIX_CHALLENGE_HI, KECCAK256_PREFIX_CHALLENGE_LO, - KECCAK256_STATE_SIZE, PERSONA_TAG, - }; - - #[cfg(not(feature = "evm"))] - fn compute_updated_state_for_testing(input: &[u8]) -> [u8; KECCAK256_STATE_SIZE] { - let input_lo = [input, &[KECCAK256_PREFIX_CHALLENGE_LO]].concat(); - let input_hi = [input, &[KECCAK256_PREFIX_CHALLENGE_HI]].concat(); - - let mut hasher_lo = Keccak256::new(); - let mut hasher_hi = Keccak256::new(); - - hasher_lo.update(&input_lo); - hasher_hi.update(&input_hi); - - let output_lo = hasher_lo.finalize(); - let output_hi = hasher_hi.finalize(); - - [output_lo, output_hi] - .concat() - .as_slice() - .try_into() - .unwrap() - } - - #[cfg(not(feature = "evm"))] - fn squeeze_for_testing( - transcript: &[u8], - round: u16, - state: [u8; KECCAK256_STATE_SIZE], - label: &'static [u8], - ) -> [u8; 64] { - let input = [ - transcript, - DOM_SEP_TAG, - round.to_le_bytes().as_ref(), - state.as_ref(), - label, - ] - .concat(); - compute_updated_state_for_testing(&input) - } - - // This test is meant to ensure compatibility between the incremental way of computing the transcript above, and - // the former, which materialized the entirety of the input vector before calling Keccak256 on it. - #[cfg(not(feature = "evm"))] - fn test_keccak_transcript_incremental_vs_explicit_with() { - let test_label = b"test"; - let mut transcript: Keccak256Transcript = Keccak256Transcript::new(test_label); - let mut rng = rand::thread_rng(); - - // ten scalars - let scalars = std::iter::from_fn(|| Some(::Scalar::from(rng.gen::()))) - .take(10) - .collect::>(); - - // add the scalars to the transcripts, - let mut manual_transcript: Vec = vec![]; - let labels = [ - b"s1", b"s2", b"s3", b"s4", b"s5", b"s6", b"s7", b"s8", b"s9", b"s0", - ]; - - for i in 0..10 { - transcript.absorb(&labels[i][..], &scalars[i]); - manual_transcript.extend(labels[i]); - manual_transcript.extend(scalars[i].to_transcript_bytes()); - } - - // compute the initial state - let input = [PERSONA_TAG, test_label].concat(); - let initial_state = compute_updated_state_for_testing(&input); - - // make a challenge - let c1: ::Scalar = transcript.squeeze(b"c1").unwrap(); - - let c1_bytes = squeeze_for_testing(&manual_transcript[..], 0u16, initial_state, b"c1"); - let to_hex = |g: E::Scalar| hex::encode(g.to_repr().as_ref()); - assert_eq!(to_hex(c1), to_hex(E::Scalar::from_uniform(&c1_bytes))); - } - - #[test] - #[cfg(not(feature = "evm"))] - fn test_keccak_transcript_incremental_vs_explicit() { - test_keccak_transcript_incremental_vs_explicit_with::(); - test_keccak_transcript_incremental_vs_explicit_with::(); - test_keccak_transcript_incremental_vs_explicit_with::(); - test_keccak_transcript_incremental_vs_explicit_with::(); - test_keccak_transcript_incremental_vs_explicit_with::(); - test_keccak_transcript_incremental_vs_explicit_with::(); - } } diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index 871b3c44a..c2770f663 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -467,3 +467,24 @@ mod tests { assert_eq!(keys_read.ck, keys.ck); } } + + +#[cfg(test)] +mod evm_tests { + use super::*; + use crate::provider::Bn256EngineKZG; + + #[test] + fn test_commitment_evm_serialization() { + type E = Bn256EngineKZG; + + let comm = Commitment::::default(); + let bytes = bincode::serde::encode_to_vec(&comm, bincode::config::legacy()).unwrap(); + + println!("Commitment serialized length in nova-snark: {} bytes", bytes.len()); + println!("Commitment hex: {}", hex::encode(&bytes[..std::cmp::min(64, bytes.len())])); + + // Always expect 64 bytes (EVM-compatible uncompressed G1 point) + assert_eq!(bytes.len(), 64, "Commitment should be 64 bytes (uncompressed G1)"); + } +} diff --git a/src/provider/traits.rs b/src/provider/traits.rs index 930336624..86358eec0 100644 --- a/src/provider/traits.rs +++ b/src/provider/traits.rs @@ -152,7 +152,7 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Scalar { - #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { use ff::PrimeField; use serde::Serialize; @@ -161,7 +161,7 @@ macro_rules! impl_traits_no_dlog_ext { bytes.serialize(serializer) } - #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use ff::PrimeField; use serde::de::Error; @@ -174,7 +174,7 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Affine { - #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -191,7 +191,7 @@ macro_rules! impl_traits_no_dlog_ext { affine.serialize(serializer) } - #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -213,13 +213,13 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Point { - #[cfg(feature = "evm")] + fn serialize(&self, serializer: S) -> Result { use $crate::traits::evm_serde::CustomSerdeTrait; <$name::Affine as CustomSerdeTrait>::serialize(&self.to_affine(), serializer) } - #[cfg(feature = "evm")] + fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use $crate::traits::evm_serde::CustomSerdeTrait; Ok(Self::from( From 631b1f91fdad3c5ee2ccbbed1299b263f91ea7e2 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 7 Jan 2026 15:45:56 -0800 Subject: [PATCH 17/24] switch to affine coordinates --- examples/test_ser.rs | 33 ---------------- src/provider/bn256_grumpkin.rs | 2 - src/provider/hyperkzg.rs | 63 +++++++++++++++--------------- src/provider/pedersen.rs | 70 ++++++++++++++++++++-------------- src/provider/traits.rs | 6 --- 5 files changed, 75 insertions(+), 99 deletions(-) delete mode 100644 examples/test_ser.rs diff --git a/examples/test_ser.rs b/examples/test_ser.rs deleted file mode 100644 index 6b22cfa2b..000000000 --- a/examples/test_ser.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Quick test to check serialization size of R1CSInstance -use nova_snark::provider::Bn256EngineKZG; -use nova_snark::traits::Engine; -use nova_snark::r1cs::R1CSInstance; -use halo2curves::group::Group; -use halo2curves::bn256::Fr; -use ff::Field; - -type E = Bn256EngineKZG; - -fn main() { - // Create an R1CSInstance with 3 elements in X - let comm_w = ::GE::generator(); - let x = vec![Fr::ONE, Fr::from(2), Fr::from(3)]; - - let instance = R1CSInstance::::new_unchecked(comm_w, x); - - // Serialize with bincode - let config = bincode::config::standard() - .with_big_endian() - .with_fixed_int_encoding(); - let bytes = bincode::serde::encode_to_vec(&instance, config).unwrap(); - - println!("Serialized R1CSInstance length: {} bytes", bytes.len()); - println!("Expected: 64 (G1) + 8 (len) + 3*32 (Fr) = 168 bytes"); - - // Print structure - println!("\nByte breakdown:"); - println!("Bytes 0-31 (should be G1.x): {:02x?}", &bytes[0..32]); - println!("Bytes 32-63 (should be G1.y): {:02x?}", &bytes[32..64]); - println!("Bytes 64-71 (should be len=3): {:02x?}", &bytes[64..72]); - println!("Bytes 72-103 (should be X[0]): {:02x?}", &bytes[72..104]); -} diff --git a/src/provider/bn256_grumpkin.rs b/src/provider/bn256_grumpkin.rs index 83e38c04c..7b214e6d3 100644 --- a/src/provider/bn256_grumpkin.rs +++ b/src/provider/bn256_grumpkin.rs @@ -162,7 +162,6 @@ impl TranscriptReprTrait for G2Affine { // CustomSerdeTrait implementations for G2 impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { - fn serialize(&self, serializer: S) -> Result { use crate::traits::evm_serde::EvmCompatSerde; use serde::{Deserialize, Serialize}; @@ -186,7 +185,6 @@ impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { affine.serialize(serializer) } - fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use crate::traits::evm_serde::EvmCompatSerde; use halo2curves::bn256::Fq2; diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index 4ab7bb9a8..52fe1b95d 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -110,22 +110,27 @@ where } /// A type that holds blinding generator +#[serde_as] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] pub struct DerandKey where E::GE: DlogGroup, { + #[serde_as(as = "EvmCompatSerde")] h: ::AffineGroupElement, } /// A KZG commitment +#[serde_as] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] pub struct Commitment where E::GE: PairingGroup, { - comm: ::GE, + #[serde_as(as = "EvmCompatSerde")] + comm: ::AffineGroupElement, } impl Commitment @@ -134,11 +139,13 @@ where { /// Creates a new commitment from the underlying group element pub fn new(comm: ::GE) -> Self { - Commitment { comm } + Commitment { + comm: comm.affine(), + } } /// Returns the commitment as a group element pub fn into_inner(self) -> ::GE { - self.comm + E::GE::group(&self.comm) } } @@ -147,7 +154,7 @@ where E::GE: PairingGroup, { fn to_coordinates(&self) -> (E::Base, E::Base, bool) { - self.comm.to_coordinates() + E::GE::group(&self.comm).to_coordinates() } } @@ -157,7 +164,7 @@ where { fn default() -> Self { Commitment { - comm: E::GE::zero(), + comm: E::GE::zero().affine(), } } } @@ -167,7 +174,7 @@ where E::GE: PairingGroup, { fn to_transcript_bytes(&self) -> Vec { - let (x, y, is_infinity) = self.comm.to_coordinates(); + let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); let is_infinity_byte = (!is_infinity).into(); [ x.to_transcript_bytes(), @@ -183,7 +190,7 @@ where E::GE: PairingGroup, { fn absorb_in_ro(&self, ro: &mut E::RO) { - let (x, y, is_infinity) = self.comm.to_coordinates(); + let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); ro.absorb(x); ro.absorb(y); ro.absorb(if is_infinity { @@ -199,7 +206,7 @@ where E::GE: PairingGroup, { fn absorb_in_ro2(&self, ro: &mut E::RO2) { - let (x, y, is_infinity) = self.comm.to_coordinates(); + let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); // we have to absorb x and y in big num format let limbs_x = to_bignat_repr(&x); @@ -221,7 +228,7 @@ where E::GE: PairingGroup, { fn mul_assign(&mut self, scalar: E::Scalar) { - let result = (self as &Commitment).comm * scalar; + let result = (E::GE::group(&self.comm) * scalar).affine(); *self = Commitment { comm: result }; } } @@ -234,7 +241,7 @@ where fn mul(self, scalar: &'b E::Scalar) -> Commitment { Commitment { - comm: self.comm * scalar, + comm: (E::GE::group(&self.comm) * scalar).affine(), } } } @@ -247,7 +254,7 @@ where fn mul(self, scalar: E::Scalar) -> Commitment { Commitment { - comm: self.comm * scalar, + comm: (E::GE::group(&self.comm) * scalar).affine(), } } } @@ -260,7 +267,7 @@ where fn add(self, other: Commitment) -> Commitment { Commitment { - comm: self.comm + other.comm, + comm: (E::GE::group(&self.comm) + E::GE::group(&other.comm)).affine(), } } } @@ -470,8 +477,9 @@ where assert!(ck.ck.len() >= v.len()); Commitment { - comm: E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r, + comm: (E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r) + .affine(), } } @@ -491,7 +499,7 @@ where .par_iter() .zip(r.par_iter()) .map(|(commit, r_i)| Commitment { - comm: *commit + (h * r_i), + comm: (*commit + (h * r_i)).affine(), }) .collect() } @@ -503,8 +511,9 @@ where ) -> Self::Commitment { assert!(ck.ck.len() >= v.len()); Commitment { - comm: E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r, + comm: (E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r) + .affine(), } } @@ -524,7 +533,7 @@ where .iter() .zip(r.iter()) .map(|(commit, r_i)| Commitment { - comm: *commit + (h * r_i), + comm: (*commit + (h * r_i)).affine(), }) .collect() } @@ -535,7 +544,7 @@ where r: &E::Scalar, ) -> Self::Commitment { Commitment { - comm: commit.comm - ::group(&dk.h) * r, + comm: (E::GE::group(&commit.comm) - ::group(&dk.h) * r).affine(), } } @@ -592,7 +601,7 @@ where res += ::group(&ck.h) * r; } - Commitment { comm: res } + Commitment { comm: res.affine() } } fn ck_to_coordinates(ck: &Self::CommitmentKey) -> Vec<(E::Base, E::Base)> { @@ -813,7 +822,7 @@ where let target_chunks = DEFAULT_TARGET_CHUNKS; let h = &div_by_monomial(f, u, target_chunks); - E::CE::commit(ck, h, &E::Scalar::ZERO).comm.affine() + E::CE::commit(ck, h, &E::Scalar::ZERO).comm }; let kzg_open_batch = |f: &[Vec], @@ -911,7 +920,7 @@ where let r = vec![E::Scalar::ZERO; ell - 1]; let com: Vec> = E::CE::batch_commit(ck, &polys[1..], r.as_slice()) .par_iter() - .map(|i| i.comm.affine()) + .map(|i| i.comm) .collect(); // Phase 2 @@ -1027,13 +1036,7 @@ where ], ] .concat(), - &[ - &[C.comm.affine()][..], - &pi.com, - &pi.w, - slice::from_ref(&vk.G), - ] - .concat(), + &[&[C.comm][..], &pi.com, &pi.w, slice::from_ref(&vk.G)].concat(), ); let R0 = E::GE::group(&pi.w[0]); @@ -1166,7 +1169,7 @@ mod tests { .with_fixed_int_encoding(); let proof_bytes = bincode::serde::encode_to_vec(&proof, config).expect("Failed to serialize proof"); - assert_eq!(proof_bytes.len(), 336); + assert_eq!(proof_bytes.len(), 464); // Change the proof and expect verification to fail let mut bad_proof = proof.clone(); diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index c2770f663..402d8802a 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -46,11 +46,14 @@ where } /// A type that holds blinding generator +#[serde_as] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(bound = "")] pub struct DerandKey where E::GE: DlogGroup, { + #[serde_as(as = "EvmCompatSerde")] h: ::AffineGroupElement, } @@ -58,9 +61,12 @@ where #[serde_as] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(bound = "")] -pub struct Commitment { +pub struct Commitment +where + E::GE: DlogGroup, +{ #[serde_as(as = "EvmCompatSerde")] - pub(crate) comm: E::GE, + pub(crate) comm: ::AffineGroupElement, } impl CommitmentTrait for Commitment @@ -68,7 +74,7 @@ where E::GE: DlogGroup, { fn to_coordinates(&self) -> (E::Base, E::Base, bool) { - self.comm.to_coordinates() + E::GE::group(&self.comm).to_coordinates() } } @@ -78,7 +84,7 @@ where { fn default() -> Self { Commitment { - comm: E::GE::zero(), + comm: E::GE::zero().affine(), } } } @@ -88,7 +94,7 @@ where E::GE: DlogGroup, { fn to_transcript_bytes(&self) -> Vec { - let (x, y, is_infinity) = self.comm.to_coordinates(); + let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); let is_infinity_byte = (!is_infinity).into(); [ x.to_transcript_bytes(), @@ -104,7 +110,7 @@ where E::GE: DlogGroup, { fn absorb_in_ro(&self, ro: &mut E::RO) { - let (x, y, is_infinity) = self.comm.to_coordinates(); + let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); ro.absorb(x); ro.absorb(y); ro.absorb(if is_infinity { @@ -120,7 +126,7 @@ where E::GE: DlogGroup, { fn absorb_in_ro2(&self, ro: &mut E::RO2) { - let (x, y, is_infinity) = self.comm.to_coordinates(); + let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); // we have to absorb x and y in big num format let limbs_x = to_bignat_repr(&x); @@ -143,7 +149,7 @@ where { fn mul_assign(&mut self, scalar: E::Scalar) { *self = Commitment { - comm: self.comm * scalar, + comm: (E::GE::group(&self.comm) * scalar).affine(), }; } } @@ -155,7 +161,7 @@ where type Output = Commitment; fn mul(self, scalar: &'b E::Scalar) -> Commitment { Commitment { - comm: self.comm * scalar, + comm: (E::GE::group(&self.comm) * scalar).affine(), } } } @@ -168,7 +174,7 @@ where fn mul(self, scalar: E::Scalar) -> Commitment { Commitment { - comm: self.comm * scalar, + comm: (E::GE::group(&self.comm) * scalar).affine(), } } } @@ -181,7 +187,7 @@ where fn add(self, other: Commitment) -> Commitment { Commitment { - comm: self.comm + other.comm, + comm: (E::GE::group(&self.comm) + E::GE::group(&other.comm)).affine(), } } } @@ -245,8 +251,9 @@ where assert!(ck.ck.len() >= v.len()); Commitment { - comm: E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r, + comm: (E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r) + .affine(), } } @@ -258,8 +265,9 @@ where assert!(ck.ck.len() >= v.len()); Commitment { - comm: E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r, + comm: (E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r) + .affine(), } } @@ -282,7 +290,7 @@ where res += ::group(&ck.h) * r; } - Commitment { comm: res } + Commitment { comm: res.affine() } } fn derandomize( @@ -291,7 +299,7 @@ where r: &E::Scalar, ) -> Self::Commitment { Commitment { - comm: commit.comm - ::group(&dk.h) * r, + comm: (E::GE::group(&commit.comm) - ::group(&dk.h) * r).affine(), } } @@ -423,10 +431,7 @@ where /// reinterprets a vector of commitments as a set of generators fn reinterpret_commitments_as_ck(c: &[Commitment]) -> Result { - let ck = (0..c.len()) - .into_par_iter() - .map(|i| c[i].comm.affine()) - .collect(); + let ck = (0..c.len()).into_par_iter().map(|i| c[i].comm).collect(); // cmt is derandomized by the point that this is called Ok(CommitmentKey { @@ -468,7 +473,6 @@ mod tests { } } - #[cfg(test)] mod evm_tests { use super::*; @@ -477,14 +481,24 @@ mod evm_tests { #[test] fn test_commitment_evm_serialization() { type E = Bn256EngineKZG; - + let comm = Commitment::::default(); let bytes = bincode::serde::encode_to_vec(&comm, bincode::config::legacy()).unwrap(); - - println!("Commitment serialized length in nova-snark: {} bytes", bytes.len()); - println!("Commitment hex: {}", hex::encode(&bytes[..std::cmp::min(64, bytes.len())])); - + + println!( + "Commitment serialized length in nova-snark: {} bytes", + bytes.len() + ); + println!( + "Commitment hex: {}", + hex::encode(&bytes[..std::cmp::min(64, bytes.len())]) + ); + // Always expect 64 bytes (EVM-compatible uncompressed G1 point) - assert_eq!(bytes.len(), 64, "Commitment should be 64 bytes (uncompressed G1)"); + assert_eq!( + bytes.len(), + 64, + "Commitment should be 64 bytes (uncompressed G1)" + ); } } diff --git a/src/provider/traits.rs b/src/provider/traits.rs index 86358eec0..7b635864c 100644 --- a/src/provider/traits.rs +++ b/src/provider/traits.rs @@ -152,7 +152,6 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Scalar { - fn serialize(&self, serializer: S) -> Result { use ff::PrimeField; use serde::Serialize; @@ -161,7 +160,6 @@ macro_rules! impl_traits_no_dlog_ext { bytes.serialize(serializer) } - fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use ff::PrimeField; use serde::de::Error; @@ -174,7 +172,6 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Affine { - fn serialize(&self, serializer: S) -> Result { use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -191,7 +188,6 @@ macro_rules! impl_traits_no_dlog_ext { affine.serialize(serializer) } - fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -213,13 +209,11 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Point { - fn serialize(&self, serializer: S) -> Result { use $crate::traits::evm_serde::CustomSerdeTrait; <$name::Affine as CustomSerdeTrait>::serialize(&self.to_affine(), serializer) } - fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use $crate::traits::evm_serde::CustomSerdeTrait; Ok(Self::from( From 2fa0e5de5cf87b0c9dce02d54e15bffd74106166 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 7 Jan 2026 20:00:51 -0800 Subject: [PATCH 18/24] update --- src/provider/hyperkzg.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index 52fe1b95d..84511a0b5 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -174,14 +174,31 @@ where E::GE: PairingGroup, { fn to_transcript_bytes(&self) -> Vec { + use crate::traits::Group; let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); - let is_infinity_byte = (!is_infinity).into(); - [ - x.to_transcript_bytes(), - y.to_transcript_bytes(), - [is_infinity_byte].to_vec(), - ] - .concat() + // Get curve parameter B to determine encoding strategy + let (_, b, _, _) = E::GE::group_params(); + + if b != E::Base::ZERO { + // For curves with B!=0 (like BN254 with B=3, Grumpkin with B=-5), + // (0, 0) doesn't lie on the curve (since 0 != 0 + 0 + B), + // so point at infinity can be safely encoded as (0, 0). + let (x, y) = if is_infinity { + (E::Base::ZERO, E::Base::ZERO) + } else { + (x, y) + }; + [x.to_transcript_bytes(), y.to_transcript_bytes()].concat() + } else { + // For curves with B=0, (0, 0) lies on the curve, so we need the is_infinity flag + let is_infinity_byte = (!is_infinity).into(); + [ + x.to_transcript_bytes(), + y.to_transcript_bytes(), + [is_infinity_byte].to_vec(), + ] + .concat() + } } } From 9bfa7b927272d7b473e660b50f84bdc4b2b110ba Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Wed, 7 Jan 2026 20:19:08 -0800 Subject: [PATCH 19/24] bring back feature gates --- src/provider/bn256_grumpkin.rs | 2 + src/provider/keccak.rs | 184 ++++++++++++++++++++++++++++++++- src/provider/traits.rs | 6 ++ 3 files changed, 187 insertions(+), 5 deletions(-) diff --git a/src/provider/bn256_grumpkin.rs b/src/provider/bn256_grumpkin.rs index 7b214e6d3..479c06fdb 100644 --- a/src/provider/bn256_grumpkin.rs +++ b/src/provider/bn256_grumpkin.rs @@ -162,6 +162,7 @@ impl TranscriptReprTrait for G2Affine { // CustomSerdeTrait implementations for G2 impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { + #[cfg(feature = "evm")] fn serialize(&self, serializer: S) -> Result { use crate::traits::evm_serde::EvmCompatSerde; use serde::{Deserialize, Serialize}; @@ -185,6 +186,7 @@ impl crate::traits::evm_serde::CustomSerdeTrait for G2Affine { affine.serialize(serializer) } + #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use crate::traits::evm_serde::EvmCompatSerde; use halo2curves::bn256::Fq2; diff --git a/src/provider/keccak.rs b/src/provider/keccak.rs index d631f6012..fe355b804 100644 --- a/src/provider/keccak.rs +++ b/src/provider/keccak.rs @@ -37,12 +37,18 @@ fn compute_updated_state(keccak_instance: Keccak256, input: &[u8]) -> [u8; KECCA let output_lo = hasher_lo.finalize(); let output_hi = hasher_hi.finalize(); - // EVM-compatible: big-endian order (hi, lo) - [output_hi, output_lo] + #[cfg(not(feature = "evm"))] + return [output_lo, output_hi] .concat() .as_slice() .try_into() - .unwrap() + .unwrap(); + #[cfg(feature = "evm")] + return [output_hi, output_lo] + .concat() + .as_slice() + .try_into() + .unwrap(); } impl TranscriptEngineTrait for Keccak256Transcript { @@ -59,9 +65,36 @@ impl TranscriptEngineTrait for Keccak256Transcript { } } + #[cfg(not(feature = "evm"))] + fn squeeze(&mut self, label: &'static [u8]) -> Result { + // we gather the full input from the round, preceded by the current state of the transcript + let input = [ + DOM_SEP_TAG, + self.round.to_le_bytes().as_ref(), + self.state.as_ref(), + label, + ] + .concat(); + let output = compute_updated_state(self.transcript.clone(), &input); + + // update state + self.round = { + if let Some(v) = self.round.checked_add(1) { + v + } else { + return Err(NovaError::InternalTranscriptError); + } + }; + self.state.copy_from_slice(&output); + self.transcript = Keccak256::new(); + + // squeeze out a challenge + Ok(E::Scalar::from_uniform(&output)) + } + + #[cfg(feature = "evm")] fn squeeze(&mut self, label: &'static [u8]) -> Result { // we gather the full input from the round, preceded by the current state of the transcript - // EVM-compatible: big-endian byte order for round let input = [ DOM_SEP_TAG, self.round.to_be_bytes().as_ref(), @@ -82,7 +115,7 @@ impl TranscriptEngineTrait for Keccak256Transcript { self.state.copy_from_slice(&output); self.transcript = Keccak256::new(); - // squeeze out a challenge (reverse for EVM compatibility) + // squeeze out a challenge output.reverse(); Ok(E::Scalar::from_uniform(&output)) } @@ -100,8 +133,61 @@ impl TranscriptEngineTrait for Keccak256Transcript { #[cfg(test)] mod tests { + use crate::{ + provider::{ + keccak::Keccak256Transcript, Bn256EngineKZG, GrumpkinEngine, PallasEngine, Secp256k1Engine, + Secq256k1Engine, VestaEngine, + }, + traits::{Engine, PrimeFieldExt, TranscriptEngineTrait, TranscriptReprTrait}, + }; + use ff::PrimeField; + use rand::Rng; use sha3::{Digest, Keccak256}; + fn test_keccak_transcript_with(expected_h1: &'static str, expected_h2: &'static str) { + let mut transcript: Keccak256Transcript = Keccak256Transcript::new(b"test"); + + // two scalars + let s1 = ::Scalar::from(2u64); + let s2 = ::Scalar::from(5u64); + + // add the scalars to the transcript + transcript.absorb(b"s1", &s1); + transcript.absorb(b"s2", &s2); + + // make a challenge + let c1: ::Scalar = transcript.squeeze(b"c1").unwrap(); + assert_eq!(hex::encode(c1.to_repr().as_ref()), expected_h1); + + // a scalar + let s3 = ::Scalar::from(128u64); + + // add the scalar to the transcript + transcript.absorb(b"s3", &s3); + + // make a challenge + let c2: ::Scalar = transcript.squeeze(b"c2").unwrap(); + assert_eq!(hex::encode(c2.to_repr().as_ref()), expected_h2); + } + + #[test] + fn test_keccak_transcript() { + test_keccak_transcript_with::( + "b67339da79ce5f6dc72ad23c8c3b4179f49655cadf92d47e79c3e7788f00f125", + "b7f033d47b3519dd6efe320b995eaad1dc11712cb9b655d2e7006ed5f86bd321", + ); + + test_keccak_transcript_with::( + "b387ba3a8b9a22b3b7544a3dbbd26a048a1d354d8dc582c64d1513335e66a205", + "73ad65097d947fe45de5241bb340bbd97b198b52cc559a9657f73c361bf8700b", + ); + + test_keccak_transcript_with::( + "f15ddd8fa1675a9e273e0ef441711005d77a5fd485f4e6cdee59760ca01493fa", + "3c019f0e557abaecc99790382974cb27132bfe038af9c4d43a33ec9c426e19f5", + ); + } + #[test] fn test_keccak_example() { let mut hasher = Keccak256::new(); @@ -112,4 +198,92 @@ mod tests { "29045a592007d0c246ef02c2223570da9522d0cf0f73282c79a1bc8f0bb2c238" ); } + + use super::{ + DOM_SEP_TAG, KECCAK256_PREFIX_CHALLENGE_HI, KECCAK256_PREFIX_CHALLENGE_LO, + KECCAK256_STATE_SIZE, PERSONA_TAG, + }; + + fn compute_updated_state_for_testing(input: &[u8]) -> [u8; KECCAK256_STATE_SIZE] { + let input_lo = [input, &[KECCAK256_PREFIX_CHALLENGE_LO]].concat(); + let input_hi = [input, &[KECCAK256_PREFIX_CHALLENGE_HI]].concat(); + + let mut hasher_lo = Keccak256::new(); + let mut hasher_hi = Keccak256::new(); + + hasher_lo.update(&input_lo); + hasher_hi.update(&input_hi); + + let output_lo = hasher_lo.finalize(); + let output_hi = hasher_hi.finalize(); + + [output_lo, output_hi] + .concat() + .as_slice() + .try_into() + .unwrap() + } + + fn squeeze_for_testing( + transcript: &[u8], + round: u16, + state: [u8; KECCAK256_STATE_SIZE], + label: &'static [u8], + ) -> [u8; 64] { + let input = [ + transcript, + DOM_SEP_TAG, + round.to_le_bytes().as_ref(), + state.as_ref(), + label, + ] + .concat(); + compute_updated_state_for_testing(&input) + } + + // This test is meant to ensure compatibility between the incremental way of computing the transcript above, and + // the former, which materialized the entirety of the input vector before calling Keccak256 on it. + fn test_keccak_transcript_incremental_vs_explicit_with() { + let test_label = b"test"; + let mut transcript: Keccak256Transcript = Keccak256Transcript::new(test_label); + let mut rng = rand::thread_rng(); + + // ten scalars + let scalars = std::iter::from_fn(|| Some(::Scalar::from(rng.gen::()))) + .take(10) + .collect::>(); + + // add the scalars to the transcripts, + let mut manual_transcript: Vec = vec![]; + let labels = [ + b"s1", b"s2", b"s3", b"s4", b"s5", b"s6", b"s7", b"s8", b"s9", b"s0", + ]; + + for i in 0..10 { + transcript.absorb(&labels[i][..], &scalars[i]); + manual_transcript.extend(labels[i]); + manual_transcript.extend(scalars[i].to_transcript_bytes()); + } + + // compute the initial state + let input = [PERSONA_TAG, test_label].concat(); + let initial_state = compute_updated_state_for_testing(&input); + + // make a challenge + let c1: ::Scalar = transcript.squeeze(b"c1").unwrap(); + + let c1_bytes = squeeze_for_testing(&manual_transcript[..], 0u16, initial_state, b"c1"); + let to_hex = |g: E::Scalar| hex::encode(g.to_repr().as_ref()); + assert_eq!(to_hex(c1), to_hex(E::Scalar::from_uniform(&c1_bytes))); + } + + #[test] + fn test_keccak_transcript_incremental_vs_explicit() { + test_keccak_transcript_incremental_vs_explicit_with::(); + test_keccak_transcript_incremental_vs_explicit_with::(); + test_keccak_transcript_incremental_vs_explicit_with::(); + test_keccak_transcript_incremental_vs_explicit_with::(); + test_keccak_transcript_incremental_vs_explicit_with::(); + test_keccak_transcript_incremental_vs_explicit_with::(); + } } diff --git a/src/provider/traits.rs b/src/provider/traits.rs index 7b635864c..930336624 100644 --- a/src/provider/traits.rs +++ b/src/provider/traits.rs @@ -152,6 +152,7 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Scalar { + #[cfg(feature = "evm")] fn serialize(&self, serializer: S) -> Result { use ff::PrimeField; use serde::Serialize; @@ -160,6 +161,7 @@ macro_rules! impl_traits_no_dlog_ext { bytes.serialize(serializer) } + #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use ff::PrimeField; use serde::de::Error; @@ -172,6 +174,7 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Affine { + #[cfg(feature = "evm")] fn serialize(&self, serializer: S) -> Result { use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -188,6 +191,7 @@ macro_rules! impl_traits_no_dlog_ext { affine.serialize(serializer) } + #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -209,11 +213,13 @@ macro_rules! impl_traits_no_dlog_ext { } impl $crate::traits::evm_serde::CustomSerdeTrait for $name::Point { + #[cfg(feature = "evm")] fn serialize(&self, serializer: S) -> Result { use $crate::traits::evm_serde::CustomSerdeTrait; <$name::Affine as CustomSerdeTrait>::serialize(&self.to_affine(), serializer) } + #[cfg(feature = "evm")] fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use $crate::traits::evm_serde::CustomSerdeTrait; Ok(Self::from( From 1ca540f95f02886f37edbc5b340a2210eb1a548b Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Thu, 8 Jan 2026 09:59:47 -0800 Subject: [PATCH 20/24] go back to GE --- src/provider/hyperkzg.rs | 54 +++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index 84511a0b5..f72ad51a1 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -130,7 +130,7 @@ where E::GE: PairingGroup, { #[serde_as(as = "EvmCompatSerde")] - comm: ::AffineGroupElement, + comm: E::GE, } impl Commitment @@ -138,14 +138,12 @@ where E::GE: PairingGroup, { /// Creates a new commitment from the underlying group element - pub fn new(comm: ::GE) -> Self { - Commitment { - comm: comm.affine(), - } + pub fn new(comm: E::GE) -> Self { + Commitment { comm } } /// Returns the commitment as a group element - pub fn into_inner(self) -> ::GE { - E::GE::group(&self.comm) + pub fn into_inner(self) -> E::GE { + self.comm } } @@ -154,7 +152,7 @@ where E::GE: PairingGroup, { fn to_coordinates(&self) -> (E::Base, E::Base, bool) { - E::GE::group(&self.comm).to_coordinates() + self.comm.to_coordinates() } } @@ -164,7 +162,7 @@ where { fn default() -> Self { Commitment { - comm: E::GE::zero().affine(), + comm: E::GE::zero(), } } } @@ -175,7 +173,7 @@ where { fn to_transcript_bytes(&self) -> Vec { use crate::traits::Group; - let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); + let (x, y, is_infinity) = self.comm.to_coordinates(); // Get curve parameter B to determine encoding strategy let (_, b, _, _) = E::GE::group_params(); @@ -207,7 +205,7 @@ where E::GE: PairingGroup, { fn absorb_in_ro(&self, ro: &mut E::RO) { - let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); + let (x, y, is_infinity) = self.comm.to_coordinates(); ro.absorb(x); ro.absorb(y); ro.absorb(if is_infinity { @@ -223,7 +221,7 @@ where E::GE: PairingGroup, { fn absorb_in_ro2(&self, ro: &mut E::RO2) { - let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); + let (x, y, is_infinity) = self.comm.to_coordinates(); // we have to absorb x and y in big num format let limbs_x = to_bignat_repr(&x); @@ -245,7 +243,7 @@ where E::GE: PairingGroup, { fn mul_assign(&mut self, scalar: E::Scalar) { - let result = (E::GE::group(&self.comm) * scalar).affine(); + let result = self.comm * scalar; *self = Commitment { comm: result }; } } @@ -258,7 +256,7 @@ where fn mul(self, scalar: &'b E::Scalar) -> Commitment { Commitment { - comm: (E::GE::group(&self.comm) * scalar).affine(), + comm: self.comm * scalar, } } } @@ -271,7 +269,7 @@ where fn mul(self, scalar: E::Scalar) -> Commitment { Commitment { - comm: (E::GE::group(&self.comm) * scalar).affine(), + comm: self.comm * scalar, } } } @@ -284,7 +282,7 @@ where fn add(self, other: Commitment) -> Commitment { Commitment { - comm: (E::GE::group(&self.comm) + E::GE::group(&other.comm)).affine(), + comm: self.comm + other.comm, } } } @@ -494,9 +492,8 @@ where assert!(ck.ck.len() >= v.len()); Commitment { - comm: (E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r) - .affine(), + comm: E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r, } } @@ -516,7 +513,7 @@ where .par_iter() .zip(r.par_iter()) .map(|(commit, r_i)| Commitment { - comm: (*commit + (h * r_i)).affine(), + comm: *commit + (h * r_i), }) .collect() } @@ -528,9 +525,8 @@ where ) -> Self::Commitment { assert!(ck.ck.len() >= v.len()); Commitment { - comm: (E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r) - .affine(), + comm: E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r, } } @@ -550,7 +546,7 @@ where .iter() .zip(r.iter()) .map(|(commit, r_i)| Commitment { - comm: (*commit + (h * r_i)).affine(), + comm: *commit + (h * r_i), }) .collect() } @@ -561,7 +557,7 @@ where r: &E::Scalar, ) -> Self::Commitment { Commitment { - comm: (E::GE::group(&commit.comm) - ::group(&dk.h) * r).affine(), + comm: commit.comm - ::group(&dk.h) * r, } } @@ -618,7 +614,7 @@ where res += ::group(&ck.h) * r; } - Commitment { comm: res.affine() } + Commitment { comm: res } } fn ck_to_coordinates(ck: &Self::CommitmentKey) -> Vec<(E::Base, E::Base)> { @@ -839,7 +835,7 @@ where let target_chunks = DEFAULT_TARGET_CHUNKS; let h = &div_by_monomial(f, u, target_chunks); - E::CE::commit(ck, h, &E::Scalar::ZERO).comm + E::CE::commit(ck, h, &E::Scalar::ZERO).comm.affine() }; let kzg_open_batch = |f: &[Vec], @@ -937,7 +933,7 @@ where let r = vec![E::Scalar::ZERO; ell - 1]; let com: Vec> = E::CE::batch_commit(ck, &polys[1..], r.as_slice()) .par_iter() - .map(|i| i.comm) + .map(|i| i.comm.affine()) .collect(); // Phase 2 @@ -1053,7 +1049,7 @@ where ], ] .concat(), - &[&[C.comm][..], &pi.com, &pi.w, slice::from_ref(&vk.G)].concat(), + &[&[C.comm.affine()][..], &pi.com, &pi.w, slice::from_ref(&vk.G)].concat(), ); let R0 = E::GE::group(&pi.w[0]); From afbc25c1b1643adcb8eb7d1661c8804e2041ef08 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Thu, 8 Jan 2026 11:09:29 -0800 Subject: [PATCH 21/24] update CI and tests --- Cargo.toml | 10 ++++-- src/provider/hyperkzg.rs | 44 +++++++++++++++++++++++-- src/provider/pedersen.rs | 69 ++++++++++++---------------------------- 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 564b99380..2c0ddfa4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ rayon = "1.10" rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } rand_chacha = "0.3" subtle = "2.6.1" -halo2curves = { version = "0.9.0", features = ["std", "bits", "derive_serde"] } generic-array = "1.2.0" num-bigint = { version = "0.4.6", features = ["serde", "rand"] } num-traits = "0.2.19" @@ -34,6 +33,13 @@ thiserror = "2.0.11" once_cell = "1.18.0" itertools = "0.14.0" +# Use halo2curves ASM on x86_64 by default; disable ASM on non-x86_64 +[target.'cfg(target_arch = "x86_64")'.dependencies] +halo2curves = { version = "0.9.0", features = ["std", "bits", "derive_serde", "asm"] } + +[target.'cfg(not(target_arch = "x86_64"))'.dependencies] +halo2curves = { version = "0.9.0", features = ["std", "bits", "derive_serde"] } + [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.15", default-features = false, features = ["js"] } @@ -73,7 +79,7 @@ name = "sumcheckeq" harness = false [features] -default = ["halo2curves/asm", "io"] +default = ["io"] flamegraph = ["pprof2/flamegraph", "pprof2/criterion"] experimental = [] evm = [] diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index f72ad51a1..1f0b4b6ca 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -1049,7 +1049,13 @@ where ], ] .concat(), - &[&[C.comm.affine()][..], &pi.com, &pi.w, slice::from_ref(&vk.G)].concat(), + &[ + &[C.comm.affine()][..], + &pi.com, + &pi.w, + slice::from_ref(&vk.G), + ] + .concat(), ); let R0 = E::GE::group(&pi.w[0]); @@ -1182,7 +1188,11 @@ mod tests { .with_fixed_int_encoding(); let proof_bytes = bincode::serde::encode_to_vec(&proof, config).expect("Failed to serialize proof"); - assert_eq!(proof_bytes.len(), 464); + + assert_eq!( + proof_bytes.len(), + if cfg!(feature = "evm") { 464 } else { 336 } + ); // Change the proof and expect verification to fail let mut bad_proof = proof.clone(); @@ -1362,3 +1372,33 @@ mod tests { } } } + +#[cfg(test)] +mod evm_tests { + use super::*; + use crate::provider::Bn256EngineKZG; + + #[test] + fn test_commitment_evm_serialization() { + type E = Bn256EngineKZG; + + let comm = Commitment::::default(); + let bytes = bincode::serde::encode_to_vec(&comm, bincode::config::legacy()).unwrap(); + + println!( + "Commitment serialized length in nova-snark: {} bytes", + bytes.len() + ); + println!( + "Commitment hex: {}", + hex::encode(&bytes[..std::cmp::min(64, bytes.len())]) + ); + + // Expect 64 bytes for EVM feature, else 32 bytes + assert_eq!( + bytes.len(), + if cfg!(feature = "evm") { 64 } else { 32 }, + "Commitment serialization length mismatch" + ); + } +} diff --git a/src/provider/pedersen.rs b/src/provider/pedersen.rs index 402d8802a..82f0e4789 100644 --- a/src/provider/pedersen.rs +++ b/src/provider/pedersen.rs @@ -66,7 +66,7 @@ where E::GE: DlogGroup, { #[serde_as(as = "EvmCompatSerde")] - pub(crate) comm: ::AffineGroupElement, + pub(crate) comm: E::GE, } impl CommitmentTrait for Commitment @@ -74,7 +74,7 @@ where E::GE: DlogGroup, { fn to_coordinates(&self) -> (E::Base, E::Base, bool) { - E::GE::group(&self.comm).to_coordinates() + self.comm.to_coordinates() } } @@ -84,7 +84,7 @@ where { fn default() -> Self { Commitment { - comm: E::GE::zero().affine(), + comm: E::GE::zero(), } } } @@ -94,7 +94,7 @@ where E::GE: DlogGroup, { fn to_transcript_bytes(&self) -> Vec { - let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); + let (x, y, is_infinity) = self.comm.to_coordinates(); let is_infinity_byte = (!is_infinity).into(); [ x.to_transcript_bytes(), @@ -110,7 +110,7 @@ where E::GE: DlogGroup, { fn absorb_in_ro(&self, ro: &mut E::RO) { - let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); + let (x, y, is_infinity) = self.comm.to_coordinates(); ro.absorb(x); ro.absorb(y); ro.absorb(if is_infinity { @@ -126,7 +126,7 @@ where E::GE: DlogGroup, { fn absorb_in_ro2(&self, ro: &mut E::RO2) { - let (x, y, is_infinity) = E::GE::group(&self.comm).to_coordinates(); + let (x, y, is_infinity) = self.comm.to_coordinates(); // we have to absorb x and y in big num format let limbs_x = to_bignat_repr(&x); @@ -149,7 +149,7 @@ where { fn mul_assign(&mut self, scalar: E::Scalar) { *self = Commitment { - comm: (E::GE::group(&self.comm) * scalar).affine(), + comm: self.comm * scalar, }; } } @@ -161,7 +161,7 @@ where type Output = Commitment; fn mul(self, scalar: &'b E::Scalar) -> Commitment { Commitment { - comm: (E::GE::group(&self.comm) * scalar).affine(), + comm: self.comm * scalar, } } } @@ -174,7 +174,7 @@ where fn mul(self, scalar: E::Scalar) -> Commitment { Commitment { - comm: (E::GE::group(&self.comm) * scalar).affine(), + comm: self.comm * scalar, } } } @@ -187,7 +187,7 @@ where fn add(self, other: Commitment) -> Commitment { Commitment { - comm: (E::GE::group(&self.comm) + E::GE::group(&other.comm)).affine(), + comm: self.comm + other.comm, } } } @@ -251,9 +251,8 @@ where assert!(ck.ck.len() >= v.len()); Commitment { - comm: (E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r) - .affine(), + comm: E::GE::vartime_multiscalar_mul(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r, } } @@ -265,9 +264,8 @@ where assert!(ck.ck.len() >= v.len()); Commitment { - comm: (E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) - + ::group(&ck.h) * r) - .affine(), + comm: E::GE::vartime_multiscalar_mul_small(v, &ck.ck[..v.len()]) + + ::group(&ck.h) * r, } } @@ -290,7 +288,7 @@ where res += ::group(&ck.h) * r; } - Commitment { comm: res.affine() } + Commitment { comm: res } } fn derandomize( @@ -299,7 +297,7 @@ where r: &E::Scalar, ) -> Self::Commitment { Commitment { - comm: (E::GE::group(&commit.comm) - ::group(&dk.h) * r).affine(), + comm: commit.comm - ::group(&dk.h) * r, } } @@ -431,7 +429,10 @@ where /// reinterprets a vector of commitments as a set of generators fn reinterpret_commitments_as_ck(c: &[Commitment]) -> Result { - let ck = (0..c.len()).into_par_iter().map(|i| c[i].comm).collect(); + let ck = (0..c.len()) + .into_par_iter() + .map(|i| c[i].comm.affine()) + .collect(); // cmt is derandomized by the point that this is called Ok(CommitmentKey { @@ -472,33 +473,3 @@ mod tests { assert_eq!(keys_read.ck, keys.ck); } } - -#[cfg(test)] -mod evm_tests { - use super::*; - use crate::provider::Bn256EngineKZG; - - #[test] - fn test_commitment_evm_serialization() { - type E = Bn256EngineKZG; - - let comm = Commitment::::default(); - let bytes = bincode::serde::encode_to_vec(&comm, bincode::config::legacy()).unwrap(); - - println!( - "Commitment serialized length in nova-snark: {} bytes", - bytes.len() - ); - println!( - "Commitment hex: {}", - hex::encode(&bytes[..std::cmp::min(64, bytes.len())]) - ); - - // Always expect 64 bytes (EVM-compatible uncompressed G1 point) - assert_eq!( - bytes.len(), - 64, - "Commitment should be 64 bytes (uncompressed G1)" - ); - } -} From 719948dcad32dc4021defc1e27259b0c58de6dbc Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Thu, 8 Jan 2026 11:10:21 -0800 Subject: [PATCH 22/24] fix clippy --- src/provider/hyperkzg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/provider/hyperkzg.rs b/src/provider/hyperkzg.rs index 1f0b4b6ca..3904436c0 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -1383,7 +1383,7 @@ mod evm_tests { type E = Bn256EngineKZG; let comm = Commitment::::default(); - let bytes = bincode::serde::encode_to_vec(&comm, bincode::config::legacy()).unwrap(); + let bytes = bincode::serde::encode_to_vec(comm, bincode::config::legacy()).unwrap(); println!( "Commitment serialized length in nova-snark: {} bytes", From d204435bdd01eca9d55b1937988255ac3d83e8f7 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Thu, 8 Jan 2026 11:11:24 -0800 Subject: [PATCH 23/24] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2c0ddfa4e..bb53bfca8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nova-snark" -version = "0.42.0" +version = "0.43.0" authors = ["Srinath Setty "] edition = "2021" description = "High-speed recursive arguments from folding schemes" From 8a85cf9ac3e69e17fc34cbb1c34e37a22d3ee582 Mon Sep 17 00:00:00 2001 From: Srinath Setty Date: Thu, 8 Jan 2026 13:55:29 -0800 Subject: [PATCH 24/24] Update src/spartan/polys/power.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/spartan/polys/power.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/spartan/polys/power.rs b/src/spartan/polys/power.rs index 1e9639c31..6cf5e457e 100644 --- a/src/spartan/polys/power.rs +++ b/src/spartan/polys/power.rs @@ -40,7 +40,25 @@ impl PowPolynomial { &self.t_pow } - /// Computes two vectors such that their outer product equals the output of the `evals` function. + /// Computes two vectors such that their outer product equals the output of the [`evals`](Self::evals) function. + /// + /// # Parameters + /// + /// - `len_left`: Length of the first (left) vector factor. This must be chosen + /// together with `len_right` so that `len_left * len_right == 2^{|t_pow|}`, + /// where `|t_pow|` is the number of variables in the polynomial + /// (`self.t_pow.len()`). If this condition is not satisfied, the function + /// will panic due to the internal assertion. + /// - `len_right`: Length of the second (right) vector factor. See `len_left` + /// for the required relation between the two lengths. + /// + /// # Returns + /// + /// A vector containing the concatenation of the two factor vectors: + /// first all entries of the left vector, followed by all entries of the + /// right vector. Conceptually, if `L` and `R` denote these two vectors, + /// then their outer product `L ⊗ R` (viewed as a flattened vector) equals + /// the evaluations returned by [`evals`](Self::evals). pub fn split_evals(&self, len_left: usize, len_right: usize) -> Vec { // Compute the number of elements in the left and right halves let ell = self.t_pow.len();