diff --git a/Cargo.toml b/Cargo.toml index 6bd61e98..bb53bfca 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" @@ -19,12 +19,12 @@ 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" 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 } @@ -33,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"] } @@ -72,7 +79,8 @@ name = "sumcheckeq" harness = false [features] -default = ["halo2curves/asm", "io"] +default = ["io"] flamegraph = ["pprof2/flamegraph", "pprof2/criterion"] experimental = [] +evm = [] io = [] diff --git a/src/constants.rs b/src/constants.rs index c458a5dd..3a286a2b 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 7bda1f5f..dcf63d61 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 bincode::{enc::write::Writer, error::EncodeError}; use ff::PrimeField; @@ -25,6 +32,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 a262a9c8..e3fd05da 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 483cf115..4f1dd989 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 9408151b..37db9d3f 100644 --- a/src/gadgets/nonnative/bignat.rs +++ b/src/gadgets/nonnative/bignat.rs @@ -58,15 +58,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, @@ -324,6 +334,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 { @@ -442,6 +453,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); @@ -657,18 +669,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, @@ -734,6 +751,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 d2d590df..e68f93d2 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 bda12483..d855b9de 100644 --- a/src/gadgets/nonnative/util.rs +++ b/src/gadgets/nonnative/util.rs @@ -9,14 +9,14 @@ use std::convert::From; #[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>, @@ -57,15 +57,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, @@ -89,6 +94,7 @@ impl Num { }) } + /// Checks that the `Num` fits in the given number of bits. pub fn fits_in_bits>( &self, mut cs: CS, @@ -207,6 +213,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 f87626b5..19c3915f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,18 +18,16 @@ 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; -// private modules -mod constants; -mod digest; -mod r1cs; - use traits::{commitment::CommitmentEngineTrait, Engine}; // some type aliases diff --git a/src/nova/mod.rs b/src/nova/mod.rs index 6cb591d7..c23d4ebd 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}; diff --git a/src/provider/bn256_grumpkin.rs b/src/provider/bn256_grumpkin.rs index 53684d3f..479c06fd 100644 --- a/src/provider/bn256_grumpkin.rs +++ b/src/provider/bn256_grumpkin.rs @@ -159,3 +159,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 halo2curves::bn256::Fq2; + 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::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 5d03cfc7..3904436c 100644 --- a/src/provider/hyperkzg.rs +++ b/src/provider/hyperkzg.rs @@ -12,6 +12,7 @@ use crate::{ errors::NovaError, gadgets::utils::to_bignat_repr, provider::traits::{DlogGroup, DlogGroupExt, PairingGroup}, + traits::evm_serde::EvmCompatSerde, traits::{ commitment::{CommitmentEngineTrait, CommitmentTrait, Len}, evaluation::EvaluationEngineTrait, @@ -31,6 +32,7 @@ use num_traits::ToPrimitive; use rand_core::OsRng; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; /// Alias to points on G1 that are in preprocessed form type G1Affine = <::GE as DlogGroup>::AffineGroupElement; @@ -79,6 +81,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 @@ -91,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: E::GE, } impl Commitment @@ -114,11 +138,11 @@ where E::GE: PairingGroup, { /// Creates a new commitment from the underlying group element - pub fn new(comm: ::GE) -> Self { + pub fn new(comm: E::GE) -> Self { Commitment { comm } } /// Returns the commitment as a group element - pub fn into_inner(self) -> ::GE { + pub fn into_inner(self) -> E::GE { self.comm } } @@ -148,14 +172,31 @@ where E::GE: PairingGroup, { fn to_transcript_bytes(&self) -> Vec { + use crate::traits::Group; let (x, y, is_infinity) = 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() + } } } @@ -202,7 +243,7 @@ where E::GE: PairingGroup, { fn mul_assign(&mut self, scalar: E::Scalar) { - let result = (self as &Commitment).comm * scalar; + let result = self.comm * scalar; *self = Commitment { comm: result }; } } @@ -575,6 +616,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 @@ -585,26 +630,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]>, } @@ -1094,6 +1147,7 @@ mod tests { } #[test] + #[cfg(not(feature = "evm"))] fn test_hyperkzg_small() { let n = 4; @@ -1134,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(), 336); + + 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(); @@ -1314,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/keccak.rs b/src/provider/keccak.rs index 48cf50fb..fe355b80 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/mercury.rs b/src/provider/mercury.rs index a9d80dd1..8b5cef31 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/provider/pedersen.rs b/src/provider/pedersen.rs index 29a42540..82f0e478 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, @@ -20,6 +21,7 @@ use num_integer::Integer; use num_traits::ToPrimitive; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; #[cfg(feature = "io")] const KEY_FILE_HEAD: [u8; 12] = *b"PEDERSEN_KEY"; @@ -44,18 +46,26 @@ 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 type that holds a commitment +#[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, } @@ -188,6 +198,32 @@ pub struct CommitmentEngine { _p: PhantomData, } +impl CommitmentKey +where + E::GE: DlogGroup, +{ + /// 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 where E::GE: DlogGroupExt, @@ -290,6 +326,10 @@ 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/provider/traits.rs b/src/provider/traits.rs index b88c435f..93033662 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,83 @@ 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::de::Error; + use serde::Deserialize; + 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::{Deserialize, Serialize}; + use serde_with::serde_as; + use $crate::traits::evm_serde::EvmCompatSerde; + + #[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::{Deserialize, Serialize}; + use serde_with::serde_as; + use $crate::traits::evm_serde::EvmCompatSerde; + + #[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 7abb4790..c4cf7a1b 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}, @@ -19,9 +20,10 @@ use once_cell::sync::OnceCell; use rand_core::OsRng; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; 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)] @@ -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,18 +67,58 @@ 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, } +/// 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 +196,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 +497,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 +534,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 +592,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 +696,7 @@ impl RelaxedR1CSWitness { } } + /// Derandomizes the witness by setting randomness to zero. pub fn derandomize(&self) -> (Self, E::Scalar, E::Scalar) { ( RelaxedR1CSWitness { @@ -658,6 +737,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 +836,7 @@ impl RelaxedR1CSInstance { } } + /// Derandomizes the instance by removing the randomness from the commitments. pub fn derandomize( &self, dk: &DerandKey, diff --git a/src/spartan/direct.rs b/src/spartan/direct.rs index 31b6b004..05a56274 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::{ @@ -22,6 +23,7 @@ use crate::{ use core::marker::PhantomData; use ff::Field; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; /// 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/math.rs b/src/spartan/math.rs index 22dbce17..5c8f63ba 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 332f243b..a75a92ae 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 84f76deb..51ac2c86 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/identity.rs b/src/spartan/polys/identity.rs index 72087551..54d43ba7 100644 --- a/src/spartan/polys/identity.rs +++ b/src/spartan/polys/identity.rs @@ -1,11 +1,16 @@ +//! Identity polynomial implementation. + use core::marker::PhantomData; use ff::PrimeField; + +/// 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 given number of variables pub fn new(ell: usize) -> Self { IdentityPolynomial { ell, @@ -13,6 +18,7 @@ impl IdentityPolynomial { } } + /// 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/mod.rs b/src/spartan/polys/mod.rs index a1a192ef..08772cfc 100644 --- a/src/spartan/polys/mod.rs +++ b/src/spartan/polys/mod.rs @@ -1,7 +1,19 @@ //! This module contains the definitions of polynomial types used in the Spartan SNARK. -pub(crate) mod eq; -pub(crate) mod identity; -pub(crate) mod masked_eq; -pub(crate) mod multilinear; -pub(crate) mod power; -pub(crate) mod univariate; + +/// Module providing the equality polynomial. +pub mod 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; + +/// Module providing power polynomial. +pub mod power; + +/// Module providing univariate polynomial +pub mod univariate; diff --git a/src/spartan/polys/multilinear.rs b/src/spartan/polys/multilinear.rs index 9f08f337..22901ca4 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 { @@ -52,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. @@ -111,17 +117,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 +149,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 27065313..6cf5e457 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,36 @@ 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::>() } - /// Computes two vectors such that their outer product equals the output of the `evals` function. - /// This code ensures + /// Returns the coordinates (powers of t) used in this polynomial. + pub fn coordinates(&self) -> &[Scalar] { + &self.t_pow + } + + /// 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(); diff --git a/src/spartan/polys/univariate.rs b/src/spartan/polys/univariate.rs index 1cd3f6ee..68aea75c 100644 --- a/src/spartan/polys/univariate.rs +++ b/src/spartan/polys/univariate.rs @@ -1,27 +1,35 @@ //! 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; -// 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] +#[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(); @@ -44,8 +52,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 +64,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 +78,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 +96,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 +107,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()); @@ -104,9 +117,10 @@ 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 +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 pub fn decompress(&self, hint: &Scalar) -> UniPoly { let mut linear_term = *hint - self.coeffs_except_linear_term[0] - self.coeffs_except_linear_term[0]; @@ -123,7 +137,10 @@ 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 @@ -141,8 +158,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); @@ -209,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); @@ -242,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/spartan/ppsnark.rs b/src/spartan/ppsnark.rs index 13e0a6f1..24e9c561 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, @@ -34,6 +35,7 @@ use itertools::Itertools as _; use once_cell::sync::OnceCell; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; 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/spartan/sumcheck.rs b/src/spartan/sumcheck.rs index 8433923e..be2c201e 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, diff --git a/src/traits/commitment.rs b/src/traits/commitment.rs index ae7f5bc1..cd9db1e7 100644 --- a/src/traits/commitment.rs +++ b/src/traits/commitment.rs @@ -131,4 +131,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)>; } diff --git a/src/traits/evm_serde.rs b/src/traits/evm_serde.rs new file mode 100644 index 00000000..859b1c35 --- /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 5a0e97e4..579b2b22 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,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>; + 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,10 +43,14 @@ 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>; + type GE: Group + + Serialize + + for<'de> Deserialize<'de> + + CustomSerdeTrait; /// A type that represents a circuit-friendly sponge that consumes /// elements from the base field